@@ -214,6 +214,7 @@ import { readDesktopPreviewBridge } from "~/desktopPreview";
214214import { usePreviewStateStore } from "~/previewStateStore" ;
215215import { useClientMode } from "~/hooks/useClientMode" ;
216216import { useTransportState } from "~/hooks/useTransportState" ;
217+ import { hasCustomThreadTitle , normalizeThreadTitle } from "~/threadTitle" ;
217218
218219const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000 ;
219220const IMAGE_SIZE_LIMIT_LABEL = `${ Math . round ( PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / ( 1024 * 1024 ) ) } MB` ;
@@ -436,6 +437,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
436437 ) ;
437438 const clearComposerDraftContent = useComposerDraftStore ( ( store ) => store . clearComposerContent ) ;
438439 const setDraftThreadContext = useComposerDraftStore ( ( store ) => store . setDraftThreadContext ) ;
440+ const setDraftThreadTitle = useComposerDraftStore ( ( store ) => store . setDraftThreadTitle ) ;
439441 const getDraftThreadByProjectId = useComposerDraftStore (
440442 ( store ) => store . getDraftThreadByProjectId ,
441443 ) ;
@@ -2374,7 +2376,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
23742376 // Stage attachments in persisted draft state first so persist middleware can write them.
23752377 syncComposerDraftPersistedAttachments ( threadId , serialized ) ;
23762378 } catch {
2377- const currentAttachmentIds = new Set ( composerAttachments . map ( ( attachment ) => attachment . id ) ) ;
2379+ const currentAttachmentIds = new Set (
2380+ composerAttachments . map ( ( attachment ) => attachment . id ) ,
2381+ ) ;
23782382 const fallbackPersistedAttachments = getPersistedAttachmentsForThread ( ) ;
23792383 const fallbackPersistedIds = fallbackPersistedAttachments
23802384 . map ( ( attachment ) => attachment . id )
@@ -2523,7 +2527,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
25232527 nextQueued . text ,
25242528 composerTerminalContextsSnapshot ,
25252529 ) ;
2526- const fallbackOutgoingText = nextQueued . attachments . some ( ( attachment ) => attachment . type === "image" )
2530+ const fallbackOutgoingText = nextQueued . attachments . some (
2531+ ( attachment ) => attachment . type === "image" ,
2532+ )
25272533 ? IMAGE_ONLY_BOOTSTRAP_PROMPT
25282534 : "" ;
25292535 const outgoingMessageText = formatOutgoingPrompt ( {
@@ -3191,18 +3197,24 @@ export default function ChatView({ threadId }: ChatViewProps) {
31913197 }
31923198 }
31933199
3194- const firstComposerAttachment = composerAttachmentsSnapshot [ 0 ] ?? null ;
3195- let titleSeed = trimmed ;
3196- if ( ! titleSeed ) {
3197- if ( firstComposerAttachment ) {
3198- titleSeed = `${ firstComposerAttachment . type === "image" ? "Image" : "File" } : ${ firstComposerAttachment . name } ` ;
3199- } else if ( composerTerminalContextsSnapshot . length > 0 ) {
3200- titleSeed = formatTerminalContextLabel ( composerTerminalContextsSnapshot [ 0 ] ! ) ;
3201- } else {
3202- titleSeed = "New thread" ;
3200+ const manualThreadTitle = hasCustomThreadTitle ( activeThread . title )
3201+ ? normalizeThreadTitle ( activeThread . title )
3202+ : null ;
3203+ let title = manualThreadTitle ;
3204+ if ( ! title ) {
3205+ const firstComposerAttachment = composerAttachmentsSnapshot [ 0 ] ?? null ;
3206+ let titleSeed = trimmed ;
3207+ if ( ! titleSeed ) {
3208+ if ( firstComposerAttachment ) {
3209+ titleSeed = `${ firstComposerAttachment . type === "image" ? "Image" : "File" } : ${ firstComposerAttachment . name } ` ;
3210+ } else if ( composerTerminalContextsSnapshot . length > 0 ) {
3211+ titleSeed = formatTerminalContextLabel ( composerTerminalContextsSnapshot [ 0 ] ! ) ;
3212+ } else {
3213+ titleSeed = normalizeThreadTitle ( null ) ;
3214+ }
32033215 }
3216+ title = truncateTitle ( titleSeed ) ;
32043217 }
3205- const title = truncateTitle ( titleSeed ) ;
32063218 let threadCreateModel : ModelSlug =
32073219 selectedModel || ( activeProject . model as ModelSlug ) || DEFAULT_MODEL_BY_PROVIDER . codex ;
32083220
@@ -3249,7 +3261,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
32493261 }
32503262
32513263 // Auto-title from first message
3252- if ( isFirstMessage && isServerThread ) {
3264+ if ( isFirstMessage && isServerThread && ! hasCustomThreadTitle ( activeThread . title ) ) {
32533265 await api . orchestration . dispatchCommand ( {
32543266 type : "thread.meta.update" ,
32553267 commandId : newCommandId ( ) ,
@@ -4327,6 +4339,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
43274339 activeProjectName = { activeProject ?. name }
43284340 activeProjectCwd = { activeProject ?. cwd }
43294341 isGitRepo = { isGitRepo }
4342+ isLocalDraftThread = { isLocalDraftThread }
43304343 openInCwd = { gitCwd }
43314344 activeProjectScripts = { activeProject ?. scripts }
43324345 preferredScriptId = {
@@ -4343,6 +4356,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
43434356 gitCwd = { gitCwd }
43444357 diffOpen = { diffOpen }
43454358 clientMode = { clientMode }
4359+ onRenameDraftThreadTitle = { ( title ) => {
4360+ setDraftThreadTitle ( activeThread . id , title ) ;
4361+ } }
43464362 onRunProjectScript = { ( script ) => {
43474363 void runProjectScript ( script ) ;
43484364 } }
0 commit comments