From 7e2db0a140dbdbcf32035f9174c3e189317f8168 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Thu, 25 Jun 2026 00:05:48 +0700 Subject: [PATCH] fix(desktop): stop refText crash on undefined composer attachment holes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A session switch or draft restore can leave undefined/null holes in the composer attachments array. AttachmentList was guarded against this in #49624, but the sibling submit path was not: submitPromptText maps the same array through attachmentDisplayText/optimisticAttachmentRef and buildContextText (a.kind / a.label / a.refText), so a hole threw "Cannot read properties of undefined (reading 'refText')" — an uncaught renderer error that blanks the chat pane and shows "Desktop app link offline". Close the whole bug class: - attachmentDisplayText / optimisticAttachmentRef no-op on a falsy attachment (shared chokepoint, also protects thread.tsx drop handler). - submitPromptText filters falsy entries from the source array, and buildContextText filters its (possibly post-sync) input before reading fields. --- .../src/app/session/hooks/use-prompt-actions.ts | 16 +++++++++++++--- apps/desktop/src/lib/chat-runtime.ts | 11 +++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts index 863854a738b..307fb7e24bb 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts @@ -555,7 +555,14 @@ export function usePromptActions({ async (rawText: string, options?: SubmitTextOptions) => { const visibleText = rawText.trim() const usingComposerAttachments = !options?.attachments - const attachments = options?.attachments ?? $composerAttachments.get() + // Drop undefined/null holes a session switch or draft restore can leave in + // the attachments array (same bug class as AttachmentList #49624). Without + // this, the sibling iterations below (a.kind / a.label / a.refText, and the + // sync step) throw "Cannot read properties of undefined (reading 'refText')" + // and break the chat surface. + const attachments = (options?.attachments ?? $composerAttachments.get()).filter( + (a): a is ComposerAttachment => Boolean(a) + ) const terminalContextBlocks = terminalContextBlocksFromDraft(rawText).join('\n\n') const hasImage = attachments.some(a => a.kind === 'image') @@ -568,14 +575,17 @@ export function usePromptActions({ let attachmentRefs = attachments.map(optimisticAttachmentRef).filter((r): r is string => Boolean(r)) const buildContextText = (atts: ComposerAttachment[]): string => { - const contextRefs = atts + // atts may be the post-sync array, which can reintroduce holes; filter + // before touching a.refText / a.kind. + const present = atts.filter((a): a is ComposerAttachment => Boolean(a)) + const contextRefs = present .map(a => a.refText) .filter(Boolean) .join('\n') return ( [contextRefs, terminalContextBlocks, visibleText].filter(Boolean).join('\n\n') || - (atts.some(a => a.kind === 'image') ? 'What do you see in this image?' : '') + (present.some(a => a.kind === 'image') ? 'What do you see in this image?' : '') ) } diff --git a/apps/desktop/src/lib/chat-runtime.ts b/apps/desktop/src/lib/chat-runtime.ts index c573a1e5899..fbf0ebdf8c0 100644 --- a/apps/desktop/src/lib/chat-runtime.ts +++ b/apps/desktop/src/lib/chat-runtime.ts @@ -155,6 +155,13 @@ export function pathLabel(path: string): string { } export function attachmentDisplayText(attachment: ComposerAttachment): string | null { + // Session switches / draft restores can leave undefined holes in the + // composer attachments array (see AttachmentList's filter(Boolean) + #49624). + // Every consumer funnels through here, so guard the chokepoint too. + if (!attachment) { + return null + } + if (attachment.kind === 'terminal' && attachment.detail) { return `\`\`\`terminal\n${attachment.detail.trim()}\n\`\`\`` } @@ -188,6 +195,10 @@ export function attachmentDisplayText(attachment: ComposerAttachment): string | * through to `attachmentDisplayText`. */ export function optimisticAttachmentRef(attachment: ComposerAttachment): string | null { + if (!attachment) { + return null + } + if (attachment.kind === 'image' && attachment.previewUrl?.startsWith('data:')) { return attachment.previewUrl }