fix(desktop): stop refText crash on undefined composer attachment holes

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.
This commit is contained in:
xxxigm 2026-06-25 00:05:48 +07:00 committed by Teknium
parent 17beb55e3c
commit 7e2db0a140
2 changed files with 24 additions and 3 deletions

View file

@ -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?' : '')
)
}

View file

@ -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
}