Skip to content

Commit f7007cb

Browse files
zmanianclaude
andcommitted
fix(title): deduplicate title-setting logic, handle empty input
Extract shared set_title_if_missing() helper into db module to eliminate duplicated title-setting code between thread_ops.rs and bridge/router.rs. Skip empty/whitespace-only input to prevent permanently blocking title-setting on image-only or attachment-only messages. Addresses PR #2348 review feedback (duplicated code, empty input blocking). [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2536616 commit f7007cb

5 files changed

Lines changed: 37 additions & 29 deletions

File tree

src/agent/thread_ops.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,19 +1078,7 @@ impl Agent {
10781078
}
10791079
}
10801080

1081-
// Set a conversation title from the first user message so the
1082-
// thread list shows a descriptive name in the sidebar.
1083-
if let Ok(None) = store
1084-
.get_conversation_metadata(thread_id)
1085-
.await
1086-
.map(|m| m.and_then(|v| v.get("title").and_then(|t| t.as_str()).map(String::from)))
1087-
{
1088-
let title_text: String = user_input.chars().take(100).collect();
1089-
let title_val = serde_json::json!(title_text);
1090-
let _ = store
1091-
.update_conversation_metadata_field(thread_id, "title", &title_val)
1092-
.await;
1093-
}
1081+
crate::db::set_title_if_missing(store.as_ref(), thread_id, user_input).await;
10941082
}
10951083

10961084
/// Persist the assistant response to the DB after the agentic loop completes.

src/bridge/router.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,20 +2969,7 @@ async fn handle_with_engine_inner(
29692969
};
29702970
if let Some(cid) = v1_conv_id {
29712971
let _ = db.add_conversation_message(cid, "user", content).await;
2972-
// Set a conversation title from the first user message so the
2973-
// thread list shows a descriptive name even if the subquery-based
2974-
// title derivation has timing issues.
2975-
if let Ok(None) = db
2976-
.get_conversation_metadata(cid)
2977-
.await
2978-
.map(|m| m.and_then(|v| v.get("title").and_then(|t| t.as_str()).map(String::from)))
2979-
{
2980-
let title_text: String = content.chars().take(100).collect();
2981-
let title_val = serde_json::json!(title_text);
2982-
let _ = db
2983-
.update_conversation_metadata_field(cid, "title", &title_val)
2984-
.await;
2985-
}
2972+
crate::db::set_title_if_missing(db.as_ref(), cid, content).await;
29862973
}
29872974
}
29882975

src/channels/web/handlers/chat.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ pub async fn chat_threads_handler(
174174
let title = t
175175
.turns
176176
.first()
177-
.map(|turn| turn.user_input.chars().take(100).collect::<String>())
177+
.map(|turn| turn.user_input.trim().chars().take(100).collect::<String>())
178178
.filter(|s| !s.is_empty());
179179
ThreadInfo {
180180
id: t.id,

src/channels/web/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1261,7 +1261,7 @@ pub(crate) async fn chat_threads_handler(
12611261
let title = t
12621262
.turns
12631263
.first()
1264-
.map(|turn| turn.user_input.chars().take(100).collect::<String>())
1264+
.map(|turn| turn.user_input.trim().chars().take(100).collect::<String>())
12651265
.filter(|s| !s.is_empty());
12661266
ThreadInfo {
12671267
id: t.id,

src/db/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,39 @@ pub trait ConversationStore: Send + Sync {
494494
) -> Result<Option<String>, DatabaseError>;
495495
}
496496

497+
/// Set a conversation title from user input if one hasn't been set yet.
498+
///
499+
/// Skips empty/whitespace-only input so that image-only or attachment-only
500+
/// messages don't permanently block title-setting with an empty string.
501+
/// Truncates to the first 100 characters for sidebar display.
502+
pub async fn set_title_if_missing(
503+
store: &(dyn ConversationStore + Send + Sync),
504+
conversation_id: Uuid,
505+
user_input: &str,
506+
) {
507+
let trimmed = user_input.trim();
508+
if trimmed.is_empty() {
509+
return;
510+
}
511+
512+
let has_title = match store.get_conversation_metadata(conversation_id).await {
513+
Ok(Some(meta)) => meta
514+
.get("title")
515+
.and_then(|t| t.as_str())
516+
.is_some_and(|s| !s.is_empty()),
517+
Ok(None) => false,
518+
Err(_) => return,
519+
};
520+
521+
if !has_title {
522+
let title_text: String = trimmed.chars().take(100).collect();
523+
let title_val = serde_json::json!(title_text);
524+
let _ = store
525+
.update_conversation_metadata_field(conversation_id, "title", &title_val)
526+
.await;
527+
}
528+
}
529+
497530
#[async_trait]
498531
pub trait JobStore: Send + Sync {
499532
async fn save_job(&self, ctx: &JobContext) -> Result<(), DatabaseError>;

0 commit comments

Comments
 (0)