From 73f8ddbb8b69c270b7bd87dc5aaf451f21caf48f Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 22:55:24 -0500 Subject: [PATCH] fix(desktop): stop hoisting an @ ref that is already inline in the prose displayContentForMessage re-derived every ref from the attached context block and re-emitted it as a detached list above the text. Now that the token survives expansion, that list duplicated the inline chip. Hoist only the refs the prose is missing, so turns persisted by an older backend still render their chips. --- apps/desktop/src/lib/chat-messages.test.ts | 13 +++++++++++++ apps/desktop/src/lib/chat-messages.ts | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/chat-messages.test.ts b/apps/desktop/src/lib/chat-messages.test.ts index 6ddabbe9b38..d6bfd77525a 100644 --- a/apps/desktop/src/lib/chat-messages.test.ts +++ b/apps/desktop/src/lib/chat-messages.test.ts @@ -220,6 +220,19 @@ describe('toChatMessages', () => { expect(chatMessageText(message)).toBe('@file:foo.ts\n\nlook') }) + it('leaves an inline @ ref in place instead of hoisting a duplicate', () => { + const [message] = toChatMessages([ + { + role: 'user', + content: + 'summarize @file:`src/main.ts` for me\n\n--- Attached Context ---\n\nšŸ“„ @file:`src/main.ts` (10 tokens)\n```ts\nconst x = 1\n```', + timestamp: 1 + } + ]) + + expect(chatMessageText(message)).toBe('summarize @file:`src/main.ts` for me') + }) + it('projects durable timeline kinds without inspecting their text', () => { const messages = toChatMessages([ { role: 'user', content: 'real user turn', timestamp: 1 }, diff --git a/apps/desktop/src/lib/chat-messages.ts b/apps/desktop/src/lib/chat-messages.ts index f0d8e2b7c64..13cca5b511b 100644 --- a/apps/desktop/src/lib/chat-messages.ts +++ b/apps/desktop/src/lib/chat-messages.ts @@ -311,7 +311,12 @@ function displayContentForMessage(role: SessionMessage['role'], content: unknown const attachedContext = textContent.slice(marker.index + marker[0].length) const refs = [...new Set(Array.from(attachedContext.matchAll(CONTEXT_REF_RE)).map(match => match[0]))] - return [refs.join('\n'), visibleText].filter(Boolean).join('\n\n') || visibleText + // The prose keeps the `@file:` token the user typed, so it already chips in + // place. Only hoist a ref the prose is missing — a turn persisted by an older + // backend that stripped the tokens. Re-listing an inline ref would chip twice. + const missing = refs.filter(ref => !visibleText.includes(ref)) + + return [missing.join('\n'), visibleText].filter(Boolean).join('\n\n') || visibleText } function transcriptContent(displayKind: SessionMessage['display_kind'], content: string): string | null {