From fda2a180e504074e1cddf3870cd19ca8361b91e5 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:54:09 -0700 Subject: [PATCH 01/27] fix(copilot): render unclosed special tags as text on completed messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The special-tag parser suppressed everything after an opening tag with no close, even after streaming ended — so an assistant message that merely MENTIONED `` in prose lost its entire remainder in the UI. The stream itself completed fine; only the render truncated. Suppression now applies only while streaming. A completed message can never finish an unclosed tag, so the marker was literal text and the remainder is rendered as-is. Originally written alongside the docs/ VFS work and reverted at review time to keep that PR to one concern; this restores it on its own. --- .../components/special-tags/special-tags.test.ts | 15 +++++++++++++++ .../components/special-tags/special-tags.tsx | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 09444ecccbf..d6a93c3ee8c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -149,6 +149,21 @@ describe('parseSpecialTags with ', () => { expect(segments).toEqual([{ type: 'text', content: 'Thinking about it. ' }]) }) + it('renders an unclosed tag as text once the message is complete', () => { + const content = + 'The `` file chip only renders when its path points to a real file.' + const { segments, hasPendingTag } = parseSpecialTags(content, false) + expect(hasPendingTag).toBe(false) + expect(segments).toEqual([ + { type: 'text', content: 'The `' }, + { + type: 'text', + content: + '` file chip only renders when its path points to a real file.', + }, + ]) + }) + it('strips a trailing partial opening tag while streaming', () => { const { segments, hasPendingTag } = parseSpecialTags('Let me ask. ` + // chip"), not a real tag. Render the remainder instead of swallowing it. + const remaining = content.slice(nearestStart) + if (remaining.trim()) { + segments.push({ type: 'text', content: remaining }) + } break } From 3645d611dc7456210df992fa0c2ea25289a18a4b Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:23:27 -0700 Subject: [PATCH 02/27] improvement(copilot): show text as soon as an unclosed tag cannot resolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restoring the text only at end of stream left a real gap: once the model mentions a tag in prose, everything after it stays invisible for the rest of the stream and reappears in one jump when streaming stops. On a long reply that is most of the message. Two properties let us decide much earlier, both conservative — they only fire on content that could not have parsed: - Tags never nest, so any special-tag marker inside the body (opening OR closing, not just a mismatched close) proves the opener was literal text. - JSON-bodied tags must start with `{` or `[`, so the first non-space character settles it — prose after the marker is caught on the very next chunk rather than at the end. The second is per-tag, not global: `thinking` bodies are prose by design (parseTextTagBody), so the JSON rule cannot apply there and only the nesting rule can rescue it. A test documents that remaining gap rather than leaving it implicit. A false positive is cheap by construction: text shows early and the end-of-stream parse still produces the correct final render. --- .../special-tags/special-tags.test.ts | 41 ++++++++++++ .../components/special-tags/special-tags.tsx | 63 +++++++++++++++++-- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index d6a93c3ee8c..cf79ecbc375 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -149,6 +149,47 @@ describe('parseSpecialTags with ', () => { expect(segments).toEqual([{ type: 'text', content: 'Thinking about it. ' }]) }) + it('shows prose immediately mid-stream instead of blanking the rest', () => { + // The failure this replaces: everything after the marker stayed invisible + // for the remainder of the stream, then reappeared when it ended. + const content = 'The `` chip only renders for a real file.' + const { segments, hasPendingTag } = parseSpecialTags(content, true) + expect(hasPendingTag).toBe(false) + expect(segments.map((s) => ('content' in s ? s.content : s.type)).join('')).toContain( + 'chip only renders for a real file.' + ) + }) + + it('still suppresses a JSON-bodied tag that is genuinely mid-stream', () => { + const { segments, hasPendingTag } = parseSpecialTags( + 'Here you go {"type":"file","id":"abc"', + true + ) + expect(hasPendingTag).toBe(true) + expect(segments).toEqual([{ type: 'text', content: 'Here you go ' }]) + }) + + it('bails when a foreign closing tag appears inside the body', () => { + // Tags never nest, so a close for a different tag proves the opener was text. + const { hasPendingTag } = parseSpecialTags( + 'see [{"title":"a","description":"b"}] more', + true + ) + expect(hasPendingTag).toBe(false) + }) + + it('bails on a nested opening tag', () => { + const { hasPendingTag } = parseSpecialTags('a b c', true) + expect(hasPendingTag).toBe(false) + }) + + it('keeps suppressing an unclosed thinking tag with prose — its body is not JSON', () => { + // Documents the deliberate gap: `thinking` bodies are prose, so the JSON + // heuristic cannot apply and only the nesting rule can rescue it. + const { hasPendingTag } = parseSpecialTags('a still reasoning about', true) + expect(hasPendingTag).toBe(true) + }) + it('renders an unclosed tag as text once the message is complete', () => { const content = 'The `` file chip only renders when its path points to a real file.' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 6906561ff8f..3355cb10327 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -473,6 +473,56 @@ function parseSpecialTagData( * Trailing partial opening tags (e.g. `([ + 'options', + 'usage_upgrade', + 'credential', + 'mothership-error', + 'workspace_resource', + 'question', +]) + +/** + * True when an opening tag with no close yet can NEVER resolve, so the text + * after it should be shown immediately instead of held back until the stream + * ends. + * + * Without this, a message that merely mentions a tag in prose goes blank from + * that point on for the rest of the stream — the text is only restored once + * streaming stops. Two facts let us decide early: + * + * 1. Tags never nest. Any other special-tag marker inside the body — opening or + * closing — means this opener was literal text, not a real tag. + * 2. JSON-bodied tags must start with `{` or `[`. The first non-space character + * settles it, so prose after the marker is caught on the very next chunk. + * + * Both are conservative: they only fire on content that could not have parsed. + * A false positive would merely show text early that a later chunk resolves + * into a tag — the end-of-stream parse still produces the correct final render. + */ +function unclosedTagCannotResolve( + tagName: (typeof SPECIAL_TAG_NAMES)[number], + body: string +): boolean { + for (const name of SPECIAL_TAG_NAMES) { + // A close for this tag is absent by definition here, so this catches a + // FOREIGN close; the open check catches nesting, including self-nesting. + if (body.includes(``) || body.includes(`<${name}>`)) return true + } + + if (JSON_BODY_TAG_NAMES.has(tagName)) { + const firstChar = body.trimStart().charAt(0) + if (firstChar !== '' && firstChar !== '{' && firstChar !== '[') return true + } + + return false +} + export function parseSpecialTags(content: string, isStreaming: boolean): ParsedSpecialContent { const segments: ContentSegment[] = [] let hasPendingTag = false @@ -526,14 +576,19 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS const closeIdx = content.indexOf(closeTag, bodyStart) if (closeIdx === -1) { - if (isStreaming) { + // Hold the text back only while a close is still plausible. A completed + // message can never finish an unclosed tag, and mid-stream the heuristics + // in unclosedTagCannotResolve rule it out early — otherwise a tag merely + // mentioned in prose blanks the rest of the message until the stream ends. + const stillResolvable = + isStreaming && + nearestTagName !== '' && + !unclosedTagCannotResolve(nearestTagName, content.slice(bodyStart)) + if (stillResolvable) { hasPendingTag = true cursor = content.length break } - // A completed message can never finish an unclosed tag — the marker was - // literal text mentioning the tag (e.g. "the `` - // chip"), not a real tag. Render the remainder instead of swallowing it. const remaining = content.slice(nearestStart) if (remaining.trim()) { segments.push({ type: 'text', content: remaining }) From 336ff5c523d96ee8d3ae2d83d4aa876e47f6f2e4 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:29:40 -0700 Subject: [PATCH 03/27] fix(copilot): ignore tag syntax quoted inside a JSON tag body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nesting rule treated any tag marker in the body as proof the opener was literal text. But a JSON body can legitimately quote tag syntax — a `` asking which tag to use, a `` whose title mentions one — and that is exactly the "model explains its own tags" situation this whole fix exists for. Bailing there is the one expensive false positive in the design: the raw JSON renders and STAYS for the rest of the stream, then snaps into a card when the real close arrives. More jarring than the bug being fixed. For JSON-bodied tags the nesting scan now runs over a copy with string literals blanked (escape-aware, and tolerant of the unterminated trailing string that is normal mid-stream). Markers in real body position still count. `thinking` is unaffected — its body is prose, so there are no strings to confuse it. Tests cover both halves: the streaming case does not bail, and the same body resolves to a question card once it closes. --- .../special-tags/special-tags.test.ts | 23 ++++++++++ .../components/special-tags/special-tags.tsx | 45 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index cf79ecbc375..80724d94b09 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -178,6 +178,29 @@ describe('parseSpecialTags with ', () => { expect(hasPendingTag).toBe(false) }) + it('does not bail on tag syntax quoted inside a JSON string', () => { + // The false positive this guards: a question whose text legitimately quotes + // another tag. Bailing would show raw JSON that later snaps into a card. + const streaming = 'ok [{"type":"single_select","prompt":"Use the tag?"' + expect(parseSpecialTags(streaming, true).hasPendingTag).toBe(true) + }) + + it('resolves that same question correctly once it closes', () => { + // The other half of the guarantee: the body the streaming case refused to + // bail on does render as a question card, so nothing flickered for nothing. + const complete = + 'ok [{"type":"single_select","prompt":"Use the tag?","options":[{"id":"y","label":"Yes"},{"id":"n","label":"No"}]}]' + const { segments } = parseSpecialTags(complete, false) + expect(segments.some((s) => s.type === 'question')).toBe(true) + }) + + it('still bails on a marker outside the JSON strings', () => { + // Escapes must not end the string early, and a marker in real body position + // is still evidence. + const streaming = 'ok [{"prompt":"a \\" quote"} ' + expect(parseSpecialTags(streaming, true).hasPendingTag).toBe(false) + }) + it('bails on a nested opening tag', () => { const { hasPendingTag } = parseSpecialTags('a b c', true) expect(hasPendingTag).toBe(false) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 3355cb10327..6b1863f51ff 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -487,6 +487,44 @@ const JSON_BODY_TAG_NAMES = new Set<(typeof SPECIAL_TAG_NAMES)[number]>([ 'question', ]) +/** + * Strip the contents of JSON string literals from `body`, replacing them with + * spaces so every other index is preserved. + * + * A JSON tag body can legitimately quote tag syntax — a `` asking + * which tag to use, or a `` whose title mentions one. Those + * markers live inside a string and say nothing about whether the tag will + * close, so the nesting rule must not see them. Tracks escapes so a `\"` inside + * a string does not end it early. Handles an unterminated trailing string, which + * is the normal state mid-stream. + */ +function blankJsonStringLiterals(body: string): string { + let out = '' + let inString = false + let escaped = false + + for (const char of body) { + if (escaped) { + escaped = false + out += ' ' + continue + } + if (char === '\\' && inString) { + escaped = true + out += ' ' + continue + } + if (char === '"') { + inString = !inString + out += '"' + continue + } + out += inString ? ' ' : char + } + + return out +} + /** * True when an opening tag with no close yet can NEVER resolve, so the text * after it should be shown immediately instead of held back until the stream @@ -509,10 +547,15 @@ function unclosedTagCannotResolve( tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string ): boolean { + // For a JSON-bodied tag, ignore markers inside string literals: quoting tag + // syntax is legitimate content, and treating it as evidence would bail on a + // tag that goes on to close correctly — showing raw JSON that then snaps into + // a rendered card. + const scannable = JSON_BODY_TAG_NAMES.has(tagName) ? blankJsonStringLiterals(body) : body for (const name of SPECIAL_TAG_NAMES) { // A close for this tag is absent by definition here, so this catches a // FOREIGN close; the open check catches nesting, including self-nesting. - if (body.includes(``) || body.includes(`<${name}>`)) return true + if (scannable.includes(``) || scannable.includes(`<${name}>`)) return true } if (JSON_BODY_TAG_NAMES.has(tagName)) { From 9c46ea84e09acc654985b01b74d93d4b68dcf351 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:16:48 -0700 Subject: [PATCH 04/27] fix(copilot): keep prose a mispaired tag would swallow, and bail on dead JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more real cases from traces, both of which the earlier heuristics missed. 1. A MATCHED pair whose body fails to parse was silently dropped, cursor and all. When the model explains tag syntax and ends with a backticked example containing a real closing tag, that example closes an EARLIER opener and three paragraphs become the "body" — which is not valid JSON, so the whole span vanished and the render resumed mid-sentence (trace b095e080). Now emitted verbatim, but only when the body contains a tag-shaped marker, which is what shows the pairing was wrong. A marker-free body that merely fails validation is a genuinely malformed agent payload and keeps being dropped — an existing test asserts that deliberately, and showing the user raw JSON there would be a regression. 2. The JSON heuristic only checked the FIRST character, so `{"type":"file"}', () => { expect(segments).toEqual([{ type: 'text', content: 'Thinking about it. ' }]) }) + it('keeps the text when a matched pair fails to parse', () => { + // Verbatim from a real message (trace b095e080). The model explained the + // tag and ended with a backticked example containing a REAL closing tag, + // which closed the earlier opener and made everything between it the body. + // That body is not valid JSON, so the segment was dropped and the render + // resumed mid-sentence at ") is what actually produces the interactive + // chip." — three paragraphs silently gone. + const raw = + 'Here you go — with the ending tag intentionally malformed as ``:\n\n' + + '{"type": "file", "path": "files/notes.md", "title": "notes.md"}\n\n' + + "Since the closing tag doesn't match the opening ``, the chat won't " + + 'recognize it as a valid resource chip. A properly matched pair ' + + '(`...`) is what actually produces the interactive chip.' + + const rendered = parseSpecialTags(raw, false) + .segments.map((segment) => ('content' in segment ? segment.content : '')) + .join('') + + expect(rendered).toContain("Since the closing tag doesn't match") + expect(rendered).toContain('A properly matched pair') + expect(rendered).toContain('"path": "files/notes.md"') + // No segment renders as a resource chip — the body was never valid. + expect(parseSpecialTags(raw, false).segments.some((s) => s.type === 'workspace_resource')).toBe( + false + ) + }) + + it('still drops a marker-free malformed payload rather than showing raw JSON', () => { + // The complement of the case above: no tag markers in the body, so this is + // a genuinely broken emission from the agent, not swallowed prose. + const { segments } = parseSpecialTags( + 'Before. {"type":"single_select"} After.', + false + ) + expect(segments).toEqual([ + { type: 'text', content: 'Before. ' }, + { type: 'text', content: ' After.' }, + ]) + }) + + it('still renders a matched pair whose body IS valid', () => { + const raw = + 'see {"type":"file","path":"files/a.md","title":"a.md"} ok' + const { segments } = parseSpecialTags(raw, false) + expect(segments.some((s) => s.type === 'workspace_resource')).toBe(true) + }) + it('shows prose immediately mid-stream instead of blanking the rest', () => { // The failure this replaces: everything after the marker stayed invisible // for the remainder of the stream, then reappeared when it ended. @@ -160,6 +207,24 @@ describe('parseSpecialTags with ', () => { ) }) + it('shows text once the JSON value has closed and stray content follows', () => { + // Verbatim shape from a real message (trace afbeefd0): the close tag was + // TRUNCATED to `{"type":"file","path":"files/notes.md"} ('content' in s ? s.content : '')).join('')).toContain( + 'I brew a cup of coffee' + ) + }) + + it('tolerates braces inside JSON strings when tracking depth', () => { + const raw = 'x {"title":"a } b","path":"files/a.md"' + expect(parseSpecialTags(raw, true).hasPendingTag).toBe(true) + }) + it('still suppresses a JSON-bodied tag that is genuinely mid-stream', () => { const { segments, hasPendingTag } = parseSpecialTags( 'Here you go {"type":"file","id":"abc"', diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 6b1863f51ff..63dbe48a576 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -473,6 +473,12 @@ function parseSpecialTagData( * Trailing partial opening tags (e.g. `` is exactly the case that matters. + */ +const TAG_SHAPED_MARKER = /<\/?[a-zA-Z][\w-]*>/ + /** * Tags whose body must be JSON. `thinking` is the exception — its body is prose * (see {@link parseTextTagBody}), so a non-JSON body there says nothing about @@ -525,6 +531,41 @@ function blankJsonStringLiterals(body: string): string { return out } +/** + * True while `body` could still grow into a single valid JSON value. + * + * Checking only the first character is not enough: a body like + * `{"type":"file"}`) || scannable.includes(`<${name}>`)) return true } - if (JSON_BODY_TAG_NAMES.has(tagName)) { - const firstChar = body.trimStart().charAt(0) - if (firstChar !== '' && firstChar !== '{' && firstChar !== '[') return true - } + if (JSON_BODY_TAG_NAMES.has(tagName) && !isViableJsonPrefix(body)) return true return false } @@ -647,6 +685,21 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS const parsedTag = parseSpecialTagData(nearestTagName, body) if (parsedTag) { segments.push(parsedTag) + } else if (TAG_SHAPED_MARKER.test(body)) { + // The body failed to parse AND contains tag markers, which means the + // close we matched was not this opener's — the model was explaining tag + // syntax and a later example like `...` + // closed an earlier opener, making paragraphs of prose the "body". + // Dropping that silently loses the text and resumes mid-sentence, so emit + // it verbatim. Streamdown escapes the markers, so it reads as literal text. + // + // A marker-free body that merely fails validation is a genuinely + // malformed payload from the agent; that keeps being dropped rather than + // showing the user raw JSON. + const literal = content.slice(nearestStart, closeIdx + closeTag.length) + if (literal.trim()) { + segments.push({ type: 'text', content: literal }) + } } cursor = closeIdx + closeTag.length From b9c88ece0de2cdcbb97b91e8c3903ad44a8393bb Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:24:47 -0700 Subject: [PATCH 05/27] fix(copilot): keep prose a tag wrapped instead of a JSON payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third real case from a trace (1206fd8a): `the gmail-agent workflow` — a matched pair whose body is plain prose. The sentence rendered as "...once I wired up to handle the welcome sequence" with its subject silently removed. The marker test could not fire (prose contains no tag markers), so it fell to the deliberate drop-malformed-payload path. But that path exists for an agent emitting BROKEN JSON, not for a tag wrapping prose. The distinction is whether the body was ever an attempted payload, which isViableJsonPrefix already answers: `{"type":"single_select"}` is a well-formed JSON value failing its shape guard and keeps being dropped; `the gmail-agent workflow` was never a payload and is emitted. --- .../special-tags/special-tags.test.ts | 13 ++++++++++ .../components/special-tags/special-tags.tsx | 24 +++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 5024bc89de7..aa87d2aba86 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -176,6 +176,19 @@ describe('parseSpecialTags with ', () => { ) }) + it('keeps prose a tag wrapped instead of a payload', () => { + // Verbatim from a real message (trace 1206fd8a): a matched pair whose body + // is plain prose, never an attempted JSON payload. The sentence read + // "...once I wired up to handle the welcome sequence" with the subject gone. + const raw = + 'once I wired up the gmail-agent workflow to handle the welcome sequence.' + const rendered = parseSpecialTags(raw, false) + .segments.map((segment) => ('content' in segment ? segment.content : '')) + .join('') + expect(rendered).toContain('the gmail-agent workflow') + expect(rendered).toContain('to handle the welcome sequence') + }) + it('still drops a marker-free malformed payload rather than showing raw JSON', () => { // The complement of the case above: no tag markers in the body, so this is // a genuinely broken emission from the agent, not swallowed prose. diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 63dbe48a576..0dc8c52050d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -685,17 +685,21 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS const parsedTag = parseSpecialTagData(nearestTagName, body) if (parsedTag) { segments.push(parsedTag) - } else if (TAG_SHAPED_MARKER.test(body)) { - // The body failed to parse AND contains tag markers, which means the - // close we matched was not this opener's — the model was explaining tag - // syntax and a later example like `...` - // closed an earlier opener, making paragraphs of prose the "body". - // Dropping that silently loses the text and resumes mid-sentence, so emit - // it verbatim. Streamdown escapes the markers, so it reads as literal text. + } else if ( + // The close we matched was not this opener's: the model was explaining tag + // syntax and a later example closed an earlier opener, making paragraphs + // of prose the "body". + TAG_SHAPED_MARKER.test(body) || + // Or the tag wrapped prose that was never an attempted payload at all. + (JSON_BODY_TAG_NAMES.has(nearestTagName) && !isViableJsonPrefix(body)) + ) { + // Either way the markers were literal text, so dropping the span loses + // real content and resumes mid-sentence. Emit it verbatim — Streamdown + // escapes the markers, so it reads as literal text. // - // A marker-free body that merely fails validation is a genuinely - // malformed payload from the agent; that keeps being dropped rather than - // showing the user raw JSON. + // A body that IS a well-formed JSON value and merely fails its shape + // guard keeps being dropped: that is a genuinely malformed payload from + // the agent, and showing the user raw JSON there would be a regression. const literal = content.slice(nearestStart, closeIdx + closeTag.length) if (literal.trim()) { segments.push({ type: 'text', content: literal }) From 30f2e68b17bbed37759f280de74402427839571d Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:33:51 -0700 Subject: [PATCH 06/27] refactor(copilot): resolve each special tag through four named outcomes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseSpecialTags had grown five inline branches, each added for a specific malformation found in a trace. The shape made "drop it" the implicit fallback, which is how spans that were never malformed payloads ended up silently swallowed. Extracts resolveTagAt, returning one of four named outcomes — segment, literal, discard, pending — so each decision is explicit and the main loop just dispatches on it. Fixes a latent bug the old shape hid: rejecting an unclosed tag ran `break`, abandoning the rest of the message, so a genuinely valid tag after a literal mention was never parsed. Resolution now resumes just past the rejected opener and scanning continues. Test added. Two behavior notes: - Rejected spans are emitted in smaller pieces. The renderer concatenates adjacent text segments into one markdown string, so this is display-neutral; the two tests that asserted exact segment arrays now assert joined text. - Each opener is judged on its own evidence. Previously one verdict ended the whole parse, so a nested opener released everything; now the outer is released immediately and the inner is a fresh candidate that can still hold mid-stream. It resolves at end of stream either way. --- .../special-tags/special-tags.test.ts | 44 ++++-- .../components/special-tags/special-tags.tsx | 146 ++++++++++-------- 2 files changed, 118 insertions(+), 72 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index aa87d2aba86..b45ebcce802 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -176,6 +176,16 @@ describe('parseSpecialTags with ', () => { ) }) + it('still parses a valid tag that follows a rejected one', () => { + // Before the rewrite, rejecting an unclosed tag abandoned the rest of the + // message, so this tag was never parsed at all. + const { segments } = parseSpecialTags( + 'I use loosely here. Anyway: [{"title":"A","description":"d"}] done.', + false + ) + expect(segments.map((segment) => segment.type)).toContain('options') + }) + it('keeps prose a tag wrapped instead of a payload', () => { // Verbatim from a real message (trace 1206fd8a): a matched pair whose body // is plain prose, never an attempted JSON payload. The sentence read @@ -279,9 +289,22 @@ describe('parseSpecialTags with ', () => { expect(parseSpecialTags(streaming, true).hasPendingTag).toBe(false) }) - it('bails on a nested opening tag', () => { - const { hasPendingTag } = parseSpecialTags('a b c', true) - expect(hasPendingTag).toBe(false) + it('rejects an opener a nested one disproves, then judges the inner on its own', () => { + // Each opener is evaluated independently. The first is disproved by the + // nested opener and its text is released immediately; the second is a fresh + // candidate that nothing has ruled out yet, so it holds mid-stream. + const streaming = parseSpecialTags('a b c', true) + expect(streaming.hasPendingTag).toBe(true) + expect( + streaming.segments.map((segment) => ('content' in segment ? segment.content : '')).join('') + ).toBe('a b ') + + // Once the stream ends nothing can close it, so the whole line is shown. + const done = parseSpecialTags('a b c', false) + expect(done.hasPendingTag).toBe(false) + expect( + done.segments.map((segment) => ('content' in segment ? segment.content : '')).join('') + ).toBe('a b c') }) it('keeps suppressing an unclosed thinking tag with prose — its body is not JSON', () => { @@ -296,14 +319,13 @@ describe('parseSpecialTags with ', () => { 'The `` file chip only renders when its path points to a real file.' const { segments, hasPendingTag } = parseSpecialTags(content, false) expect(hasPendingTag).toBe(false) - expect(segments).toEqual([ - { type: 'text', content: 'The `' }, - { - type: 'text', - content: - '` file chip only renders when its path points to a real file.', - }, - ]) + // Asserted on the joined text, not segment boundaries: the renderer + // concatenates adjacent text segments, so how the span is split is not + // observable to a reader. + expect(segments.every((segment) => segment.type === 'text')).toBe(true) + expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( + content + ) }) it('strips a trailing partial opening tag while streaming', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 0dc8c52050d..1c39daaa263 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -604,11 +604,83 @@ function unclosedTagCannotResolve( return false } +/** + * How one opening tag resolved. Naming the four outcomes is the point: the + * parser previously decided each case inline, which is how "drop it" quietly + * became the fallback for situations that were never malformed payloads. + */ +type TagResolution = + /** Body parsed; emit the typed segment and resume after the closing tag. */ + | { outcome: 'segment'; segment: ContentSegment; resumeAt: number } + /** Provably not a tag; render the span verbatim and resume after it. */ + | { outcome: 'literal'; resumeAt: number } + /** A well-formed payload that failed its shape guard — dropped deliberately. */ + | { outcome: 'discard'; resumeAt: number } + /** Still streaming and a close remains plausible; suppress the remainder. */ + | { outcome: 'pending' } + +/** + * True when a failed body was never an attempted payload — so the markers were + * literal text and the span must be shown rather than swallowed. + * + * Either the close we matched belongs to a different opener (the body carries + * tag markers), or the tag wrapped prose that was never JSON to begin with. + */ +function bodyIsLiteralText(tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string): boolean { + if (TAG_SHAPED_MARKER.test(body)) return true + return JSON_BODY_TAG_NAMES.has(tagName) && !isViableJsonPrefix(body) +} + +function resolveTagAt( + content: string, + openIndex: number, + tagName: (typeof SPECIAL_TAG_NAMES)[number], + isStreaming: boolean +): TagResolution { + const openTag = `<${tagName}>` + const closeTag = `` + const bodyStart = openIndex + openTag.length + const closeIdx = content.indexOf(closeTag, bodyStart) + + if (closeIdx === -1) { + if (isStreaming && !unclosedTagCannotResolve(tagName, content.slice(bodyStart))) { + return { outcome: 'pending' } + } + // Nothing can close it, so only the opener itself is literal. Resuming just + // past it (rather than abandoning the message) keeps a genuinely valid tag + // later in the same reply parseable. + return { outcome: 'literal', resumeAt: bodyStart } + } + + const resumeAt = closeIdx + closeTag.length + const body = content.slice(bodyStart, closeIdx) + + const parsed = parseSpecialTagData(tagName, body) + if (parsed) return { outcome: 'segment', segment: parsed, resumeAt } + + if (bodyIsLiteralText(tagName, body)) return { outcome: 'literal', resumeAt } + + // A well-formed value that failed its shape guard is a broken emission from + // the agent; showing the user raw JSON there would be worse than nothing. + return { outcome: 'discard', resumeAt } +} + +/** + * Splits streamed text into renderable segments, extracting complete special + * tags and deciding what to do with the ones that never resolve. + * + * Adjacent text segments are concatenated by the renderer, so emitting a span + * as several pieces is display-neutral. + */ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedSpecialContent { const segments: ContentSegment[] = [] let hasPendingTag = false let cursor = 0 + const pushText = (text: string) => { + if (text.trim()) segments.push({ type: 'text', content: text }) + } + while (cursor < content.length) { let nearestStart = -1 let nearestTagName: (typeof SPECIAL_TAG_NAMES)[number] | '' = '' @@ -621,10 +693,11 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS } } - if (nearestStart === -1) { + if (nearestStart === -1 || nearestTagName === '') { let remaining = content.slice(cursor) if (isStreaming) { + // Hide a half-arrived opening marker so it does not flash as text. const partial = remaining.match(/<[a-z_-]*$/i) if (partial) { const fragment = partial[0].slice(1) @@ -638,75 +711,26 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS } } - if (remaining.trim()) { - segments.push({ type: 'text', content: remaining }) - } + pushText(remaining) break } - if (nearestStart > cursor) { - const text = content.slice(cursor, nearestStart) - if (text.trim()) { - segments.push({ type: 'text', content: text }) - } - } + pushText(content.slice(cursor, nearestStart)) - const openTag = `<${nearestTagName}>` - const closeTag = `` - const bodyStart = nearestStart + openTag.length - const closeIdx = content.indexOf(closeTag, bodyStart) - - if (closeIdx === -1) { - // Hold the text back only while a close is still plausible. A completed - // message can never finish an unclosed tag, and mid-stream the heuristics - // in unclosedTagCannotResolve rule it out early — otherwise a tag merely - // mentioned in prose blanks the rest of the message until the stream ends. - const stillResolvable = - isStreaming && - nearestTagName !== '' && - !unclosedTagCannotResolve(nearestTagName, content.slice(bodyStart)) - if (stillResolvable) { - hasPendingTag = true - cursor = content.length - break - } - const remaining = content.slice(nearestStart) - if (remaining.trim()) { - segments.push({ type: 'text', content: remaining }) - } + const resolution = resolveTagAt(content, nearestStart, nearestTagName, isStreaming) + + if (resolution.outcome === 'pending') { + hasPendingTag = true break } - const body = content.slice(bodyStart, closeIdx) - if (!nearestTagName) { - cursor = closeIdx + closeTag.length - continue - } - const parsedTag = parseSpecialTagData(nearestTagName, body) - if (parsedTag) { - segments.push(parsedTag) - } else if ( - // The close we matched was not this opener's: the model was explaining tag - // syntax and a later example closed an earlier opener, making paragraphs - // of prose the "body". - TAG_SHAPED_MARKER.test(body) || - // Or the tag wrapped prose that was never an attempted payload at all. - (JSON_BODY_TAG_NAMES.has(nearestTagName) && !isViableJsonPrefix(body)) - ) { - // Either way the markers were literal text, so dropping the span loses - // real content and resumes mid-sentence. Emit it verbatim — Streamdown - // escapes the markers, so it reads as literal text. - // - // A body that IS a well-formed JSON value and merely fails its shape - // guard keeps being dropped: that is a genuinely malformed payload from - // the agent, and showing the user raw JSON there would be a regression. - const literal = content.slice(nearestStart, closeIdx + closeTag.length) - if (literal.trim()) { - segments.push({ type: 'text', content: literal }) - } + if (resolution.outcome === 'segment') { + segments.push(resolution.segment) + } else if (resolution.outcome === 'literal') { + pushText(content.slice(nearestStart, resolution.resumeAt)) } - cursor = closeIdx + closeTag.length + cursor = resolution.resumeAt } if (segments.length === 0 && !hasPendingTag) { From bc15e45ff600eac41a18219cc50e41397f730fb2 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:54:48 -0700 Subject: [PATCH 07/27] test(copilot): pin the no-closing-tag case as lossless MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth malformation from a trace (220cc02d): the model wrote an opening tag with valid JSON and no closing tag of any kind. No marker rule can fire — there is no marker — but the JSON value completes and prose follows, which the depth rule settles at the first space after the `}`. Already handled by that rule; this pins it. Asserted as lossless rather than merely visible: mid-stream and complete, every character of the message survives, so nothing waits for the stream to end. --- .../components/special-tags/special-tags.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index b45ebcce802..b1d7d84f920 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -186,6 +186,22 @@ describe('parseSpecialTags with ', () => { expect(segments.map((segment) => segment.type)).toContain('options') }) + it('loses nothing when the model writes no closing tag at all', () => { + // Verbatim from a real message (trace 220cc02d). No close tag exists, so no + // marker rule can fire — but the JSON value completes and prose follows, + // which settles it at the first space. Asserted as LOSSLESS: mid-stream and + // complete, every character survives. + const raw = + 'The dataset lives in {"type": "file", "path": "files/notes.md"} and I keep coming back to it whenever I need a quick reference. It never quite has everything.' + const join = (result: ReturnType) => + result.segments.map((segment) => ('content' in segment ? segment.content : '')).join('') + + const streaming = parseSpecialTags(raw, true) + expect(streaming.hasPendingTag).toBe(false) + expect(join(streaming)).toBe(raw) + expect(join(parseSpecialTags(raw, false))).toBe(raw) + }) + it('keeps prose a tag wrapped instead of a payload', () => { // Verbatim from a real message (trace 1206fd8a): a matched pair whose body // is plain prose, never an attempted JSON payload. The sentence read From 3335d70e8c97d67cdcf0cc2092d0a097e54e23ea Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:58:21 -0700 Subject: [PATCH 08/27] fix(copilot): stop the literal path flashing payloads and rescanning the buffer Review round on the four-outcome rewrite. Three defects in how an opener is judged, plus the cost of judging it. A valid tag showed its raw payload as text while its closing marker streamed in. The JSON value closes at the `}`, so a half-arrived ` block. dropArrivingClose now ignores a trailing fragment that could still grow into this tag's own close. Evidence that a close is genuinely wrong still lands immediately: a misspelled `` is not a prefix of ``, and a truncated `', () => { it('still drops a marker-free malformed payload rather than showing raw JSON', () => { // The complement of the case above: no tag markers in the body, so this is // a genuinely broken emission from the agent, not swallowed prose. - const { segments } = parseSpecialTags( + const { segments, hasPendingTag } = parseSpecialTags( 'Before. {"type":"single_select"} After.', false ) + expect(hasPendingTag).toBe(false) expect(segments).toEqual([ { type: 'text', content: 'Before. ' }, { type: 'text', content: ' After.' }, ]) }) + it('drops that same payload even when its JSON quotes tag syntax', () => { + // The marker scan must blank JSON strings the way the streaming path does. + // Scanning the raw body sees `` inside the prompt, calls the span + // literal text, and renders the raw payload — the outcome `discard` exists + // to prevent. + const { segments } = parseSpecialTags( + 'A [{"type":"single_select","prompt":"use here?"}] B', + false + ) + expect(segments).toEqual([ + { type: 'text', content: 'A ' }, + { type: 'text', content: ' B' }, + ]) + }) + + it('does not flash the payload while the closing tag is still arriving', () => { + // Each frame below is a real mid-stream state: the JSON value has closed, so + // without tolerating an arriving close the trailing ``. + for (const fragment of ['<', '[{"title":"a","description":"b"}]${fragment}`, + true + ) + expect(hasPendingTag).toBe(true) + expect(segments.map((s) => ('content' in s ? s.content : '')).join('')).toBe('see ') + } + }) + + it('still rejects a close whose name is wrong rather than merely unfinished', () => { + // The counterpart to the case above: `` can never grow + // into ``, so it settles immediately instead of hiding + // the rest of the message for the remainder of the stream. + const { hasPendingTag } = parseSpecialTags( + 'see {"type":"file","path":"a.md"} and then prose.', + true + ) + expect(hasPendingTag).toBe(false) + }) + + it('keeps a valid tag whose close an earlier broken tag would borrow', () => { + // The first opener misspells its close, so it reaches forward and matches + // the SECOND tag's close, swallowing a perfectly good resource into one + // literal span. Resuming past the opener re-scans the interior instead. + const raw = + 'See {"type":"file","path":"a.md"}\n' + + 'and a real one: {"type":"file","path":"b.md","title":"b"}' + const { segments } = parseSpecialTags(raw, false) + expect(segments.some((s) => s.type === 'workspace_resource')).toBe(true) + expect(segments.map((s) => ('content' in s ? s.content : '')).join('')).toContain( + '' + ) + }) + it('still renders a matched pair whose body IS valid', () => { const raw = 'see {"type":"file","path":"files/a.md","title":"a.md"} ok' @@ -275,10 +330,10 @@ describe('parseSpecialTags with ', () => { it('bails when a foreign closing tag appears inside the body', () => { // Tags never nest, so a close for a different tag proves the opener was text. - const { hasPendingTag } = parseSpecialTags( - 'see [{"title":"a","description":"b"}] more', - true - ) + // The array is left UNCLOSED on purpose: with a closed value the JSON rule + // would independently reject the trailing content, and this test would still + // pass with the nesting rule deleted. Unclosed, only nesting can explain it. + const { hasPendingTag } = parseSpecialTags('see [{"title":"a" more', true) expect(hasPendingTag).toBe(false) }) @@ -349,18 +404,6 @@ describe('parseSpecialTags with ', () => { expect(hasPendingTag).toBe(true) expect(segments).toEqual([{ type: 'text', content: 'Let me ask. ' }]) }) - - it('drops a question tag with an invalid body but keeps surrounding text', () => { - const { segments, hasPendingTag } = parseSpecialTags( - 'Before. {"type":"single_select"} After.', - false - ) - expect(hasPendingTag).toBe(false) - expect(segments).toEqual([ - { type: 'text', content: 'Before. ' }, - { type: 'text', content: ' After.' }, - ]) - }) }) describe('service_account credential tag', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 1c39daaa263..7dbae91aae6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -464,15 +464,6 @@ function parseSpecialTagData( return null } -/** - * Parses inline special tags (``, ``, ``) from streamed - * text content. Complete tags are extracted into typed segments; incomplete - * tags (still streaming) are suppressed from display and flagged via - * `hasPendingTag` so the caller can show a loading indicator. - * - * Trailing partial opening tags (e.g. `` is exactly the case that matters. @@ -480,18 +471,36 @@ function parseSpecialTagData( const TAG_SHAPED_MARKER = /<\/?[a-zA-Z][\w-]*>/ /** - * Tags whose body must be JSON. `thinking` is the exception — its body is prose - * (see {@link parseTextTagBody}), so a non-JSON body there says nothing about - * whether a close is still coming. + * The one tag whose body is prose rather than JSON (see {@link parseTextTagBody}), + * so a non-JSON body there says nothing about whether a close is still coming. */ -const JSON_BODY_TAG_NAMES = new Set<(typeof SPECIAL_TAG_NAMES)[number]>([ - 'options', - 'usage_upgrade', - 'credential', - 'mothership-error', - 'workspace_resource', - 'question', -]) +const PROSE_BODY_TAG_NAME: (typeof SPECIAL_TAG_NAMES)[number] = 'thinking' + +/** + * Tags whose body must be JSON. + * + * Derived from {@link SPECIAL_TAG_NAMES} rather than hand-listed: a new tag is + * JSON-bodied by default, so forgetting to update this set cannot silently + * downgrade it to the weaker prose heuristics. Opting a tag out is an explicit + * edit to {@link PROSE_BODY_TAG_NAME}. + */ +const JSON_BODY_TAG_NAMES: ReadonlySet<(typeof SPECIAL_TAG_NAMES)[number]> = new Set( + SPECIAL_TAG_NAMES.filter((name) => name !== PROSE_BODY_TAG_NAME) +) + +/** + * How much of an unclosed body to inspect per parse. + * + * Both rules in {@link unclosedTagCannotResolve} decide on their FIRST piece of + * evidence — the first foreign marker, or the first character that breaks JSON + * viability — so a bounded window reaches the same verdict as the full remainder + * for any real payload. Unbounded, the check is O(remaining length) and runs + * once per opener inside a parse that re-runs for every streamed chunk, so a + * long reply repeatedly mentioning a tag name in prose costs seconds of + * main-thread time. Evidence past the window only defers the decision to a later + * chunk, which is the same conservative direction the rules already take. + */ +const MAX_UNCLOSED_BODY_SCAN = 4096 /** * Strip the contents of JSON string literals from `body`, replacing them with @@ -544,7 +553,15 @@ function blankJsonStringLiterals(body: string): string { * do not affect the depth count. */ function isViableJsonPrefix(body: string): boolean { - const scannable = blankJsonStringLiterals(body) + return isViableJsonPrefixOf(blankJsonStringLiterals(body)) +} + +/** + * {@link isViableJsonPrefix} for a body whose string literals are already + * blanked. Callers that had to blank the body for their own scan pass the result + * straight through instead of paying for a second pass over the same text. + */ +function isViableJsonPrefixOf(scannable: string): boolean { if (scannable.trim() === '') return true const firstChar = scannable.trimStart().charAt(0) @@ -577,8 +594,10 @@ function isViableJsonPrefix(body: string): boolean { * * 1. Tags never nest. Any other special-tag marker inside the body — opening or * closing — means this opener was literal text, not a real tag. - * 2. JSON-bodied tags must start with `{` or `[`. The first non-space character - * settles it, so prose after the marker is caught on the very next chunk. + * 2. A JSON body must stay a viable JSON prefix. Depth is tracked rather than + * testing the first character alone, so a body whose top-level value has + * already closed is caught the moment stray content follows it — including a + * misspelled close like ``, which no marker rule sees. * * Both are conservative: they only fire on content that could not have parsed. * A false positive would merely show text early that a later chunk resolves @@ -588,22 +607,44 @@ function unclosedTagCannotResolve( tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string ): boolean { + const pending = dropArrivingClose(body, ``) // For a JSON-bodied tag, ignore markers inside string literals: quoting tag // syntax is legitimate content, and treating it as evidence would bail on a // tag that goes on to close correctly — showing raw JSON that then snaps into // a rendered card. - const scannable = JSON_BODY_TAG_NAMES.has(tagName) ? blankJsonStringLiterals(body) : body + const isJsonBodied = JSON_BODY_TAG_NAMES.has(tagName) + const scannable = isJsonBodied ? blankJsonStringLiterals(pending) : pending for (const name of SPECIAL_TAG_NAMES) { // A close for this tag is absent by definition here, so this catches a // FOREIGN close; the open check catches nesting, including self-nesting. if (scannable.includes(``) || scannable.includes(`<${name}>`)) return true } - if (JSON_BODY_TAG_NAMES.has(tagName) && !isViableJsonPrefix(body)) return true + if (isJsonBodied && !isViableJsonPrefixOf(scannable)) return true return false } +/** + * Drop a trailing fragment that could still grow into `closeTag`. + * + * Mid-stream the closing marker arrives a character at a time, so a body sits at + * `]` completes. That fragment is an + * arriving close, not stray content — counting it as fatal is what made a + * perfectly valid tag show its raw payload as text until the final `>` landed. + * + * Only a fragment at the very END is dropped, so evidence that the close is + * genuinely wrong still lands immediately: a misspelled `` + * is not a prefix of ``, and a truncated ` 0; n--) { + if (body.endsWith(closeTag.slice(0, n))) return body.slice(0, -n) + } + return body +} + /** * How one opening tag resolved. Naming the four outcomes is the point: the * parser previously decided each case inline, which is how "drop it" quietly @@ -620,30 +661,76 @@ type TagResolution = | { outcome: 'pending' } /** - * True when a failed body was never an attempted payload — so the markers were - * literal text and the span must be shown rather than swallowed. + * Why a failed body was never an attempted payload — so the markers were literal + * text and the span must be shown rather than swallowed. `null` means the body + * really was a payload that failed its shape guard. + * + * The two reasons resume differently, which is why they are distinguished + * rather than collapsed into a boolean (see {@link resolveTagAt}). + */ +type LiteralTextReason = + /** The body carries tag markers, so the close we matched belongs elsewhere. */ + | 'foreign-markers' + /** The tag wrapped prose that was never JSON to begin with. */ + | 'never-a-payload' + +function literalTextReason( + tagName: (typeof SPECIAL_TAG_NAMES)[number], + body: string +): LiteralTextReason | null { + const isJsonBodied = JSON_BODY_TAG_NAMES.has(tagName) + // Markers inside a JSON string are content, not evidence — a `` may + // legitimately quote tag syntax in its prompt. Scanning the raw body here + // would classify a broken payload as literal text and render it as raw JSON, + // which is exactly what `discard` exists to prevent. Mirrors the same blanking + // in unclosedTagCannotResolve, which judges the same body mid-stream. + const scannable = isJsonBodied ? blankJsonStringLiterals(body) : body + if (TAG_SHAPED_MARKER.test(scannable)) return 'foreign-markers' + if (isJsonBodied && !isViableJsonPrefixOf(scannable)) return 'never-a-payload' + return null +} + +/** + * `content.indexOf(needle, from)` memoized per needle. * - * Either the close we matched belongs to a different opener (the body carries - * tag markers), or the tag wrapped prose that was never JSON to begin with. + * The opener scan and the close lookup search the same handful of markers over + * and over as the cursor advances. A needle absent from the message resolves to + * -1 once and is never searched again; a present one is re-searched only when + * the cursor passes its last hit. Unmemoized, each lookup rescans to the end of + * the buffer for every opener, which is quadratic on a message that mentions a + * tag name many times — and this parse re-runs for every streamed chunk. + * + * Safe because `from` only ever increases within a parse: a cached -1 stays -1, + * and a cached hit at or after `from` is still the first hit at or after `from`. */ -function bodyIsLiteralText(tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string): boolean { - if (TAG_SHAPED_MARKER.test(body)) return true - return JSON_BODY_TAG_NAMES.has(tagName) && !isViableJsonPrefix(body) +function memoizedIndexOf( + cache: Map, + content: string, + needle: string, + from: number +): number { + const cached = cache.get(needle) + if (cached !== undefined && (cached === -1 || cached >= from)) return cached + const idx = content.indexOf(needle, from) + cache.set(needle, idx) + return idx } function resolveTagAt( content: string, openIndex: number, tagName: (typeof SPECIAL_TAG_NAMES)[number], - isStreaming: boolean + isStreaming: boolean, + closeCache: Map ): TagResolution { const openTag = `<${tagName}>` const closeTag = `` const bodyStart = openIndex + openTag.length - const closeIdx = content.indexOf(closeTag, bodyStart) + const closeIdx = memoizedIndexOf(closeCache, content, closeTag, bodyStart) if (closeIdx === -1) { - if (isStreaming && !unclosedTagCannotResolve(tagName, content.slice(bodyStart))) { + const inspectable = content.slice(bodyStart, bodyStart + MAX_UNCLOSED_BODY_SCAN) + if (isStreaming && !unclosedTagCannotResolve(tagName, inspectable)) { return { outcome: 'pending' } } // Nothing can close it, so only the opener itself is literal. Resuming just @@ -658,7 +745,16 @@ function resolveTagAt( const parsed = parseSpecialTagData(tagName, body) if (parsed) return { outcome: 'segment', segment: parsed, resumeAt } - if (bodyIsLiteralText(tagName, body)) return { outcome: 'literal', resumeAt } + const reason = literalTextReason(tagName, body) + if (reason === 'foreign-markers') { + // A marker in the body proves the close we matched opened somewhere else — + // this opener reached past its own missing close and borrowed a later tag's. + // Resuming past the OPENER instead of past that borrowed close re-scans the + // interior, so the genuine tag inside still renders instead of being + // swallowed into one literal span. + return { outcome: 'literal', resumeAt: bodyStart } + } + if (reason === 'never-a-payload') return { outcome: 'literal', resumeAt } // A well-formed value that failed its shape guard is a broken emission from // the agent; showing the user raw JSON there would be worse than nothing. @@ -667,7 +763,10 @@ function resolveTagAt( /** * Splits streamed text into renderable segments, extracting complete special - * tags and deciding what to do with the ones that never resolve. + * tags and deciding what to do with the ones that never resolve. Incomplete + * tags are suppressed and flagged via `hasPendingTag` so the caller can show a + * loading indicator, and a trailing partial opening marker (`() + const closeCache = new Map() + while (cursor < content.length) { let nearestStart = -1 let nearestTagName: (typeof SPECIAL_TAG_NAMES)[number] | '' = '' for (const name of SPECIAL_TAG_NAMES) { - const idx = content.indexOf(`<${name}>`, cursor) + const idx = memoizedIndexOf(openerCache, content, `<${name}>`, cursor) if (idx !== -1 && (nearestStart === -1 || idx < nearestStart)) { nearestStart = idx nearestTagName = name @@ -717,7 +819,7 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS pushText(content.slice(cursor, nearestStart)) - const resolution = resolveTagAt(content, nearestStart, nearestTagName, isStreaming) + const resolution = resolveTagAt(content, nearestStart, nearestTagName, isStreaming, closeCache) if (resolution.outcome === 'pending') { hasPendingTag = true From 156fbc04d9ff6d113f9397544e39d9ee2938c09a Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:04:48 -0700 Subject: [PATCH 09/27] refactor(copilot): drop the JSON-prefix wrapper the split orphaned Splitting isViableJsonPrefixOf out so callers that had already blanked the body would not pay for a second pass left isViableJsonPrefix behind with no callers. Both call sites blank the body for their own marker scan first, so the wrapper has nothing left to do. Biome does not flag it, so it would have sat there indefinitely. The wrapper's docstring carried the actual argument for depth tracking over a first-character check, which is the non-obvious part of the function; it moves onto isViableJsonPrefixOf rather than being deleted with it, along with the already-blanked precondition its callers rely on. Co-Authored-By: Claude Opus 5 (1M context) --- .../components/special-tags/special-tags.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 7dbae91aae6..9c57cae7b39 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -541,7 +541,7 @@ function blankJsonStringLiterals(body: string): string { } /** - * True while `body` could still grow into a single valid JSON value. + * True while `scannable` could still grow into a single valid JSON value. * * Checking only the first character is not enough: a body like * `{"type":"file"}`, which no marker rule sees. + * misspelled close like `` instead of ``, which no marker rule sees. * * Both are conservative: they only fire on content that could not have parsed. * A false positive would merely show text early that a later chunk resolves From e71501abe0499544bbdf4cc5d973b77a3af38c33 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:16:11 -0700 Subject: [PATCH 10/27] refactor(copilot): drop the nested-marker scan the JSON rule already covers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unclosed-tag check ran two rules over every JSON body: a scan for nested special-tag markers, then a JSON-viability test. The scan earned nothing there. A marker outside a string literal is stray content the viability rule already rejects, and a marker inside one is legitimate quoted syntax the scan is explicitly blanked to ignore. It cost 14 substring scans per opener per streamed chunk to catch nothing. Verified by deletion rather than by argument: with the scan disabled for JSON-bodied tags, the only failures were the two tests written for the scan itself, and neither is trace-derived. One documents its own contrivance — the array is left unclosed on purpose because a closed value would let the JSON rule decide it. Every test pinned from a real message still passes. The rule stays for the prose-bodied tag, where a body that is not JSON leaves nesting as the only available evidence. The foreign-close test moves to to cover it there; the marker-outside-strings test goes, since its premise was the scan. One narrow case regresses: stray non-JSON content inside a still-open structure (`[{...} `) now stays hidden until the stream ends rather than settling mid-stream. Depth tracking counts brackets, it does not validate syntax. Self-healing at end of stream, and buying it back means a real JSON prefix validator — more machinery than the case is worth. Also pins two behaviors that had no coverage and would have been "simplified" away. The two literal reasons resume at different offsets: never-a-payload must resume past the CLOSE, because resuming past the opener rescans an interior whose quoted markers the blanked scan never saw, re-parsing a tag inside a JSON string and dropping it. Collapsing the reasons passes all 54 existing tests and silently deletes text. Escape handling in the string blanker is likewise only observable through depth skew. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 42 ++++++++++++----- .../components/special-tags/special-tags.tsx | 45 ++++++++++--------- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index ddc71a9ea95..677fd0fe5a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -202,6 +202,22 @@ describe('parseSpecialTags with ', () => { expect(join(parseSpecialTags(raw, false))).toBe(raw) }) + it('does not rescan the interior of a body that carried no markers', () => { + // Pins WHY the two literal reasons resume at different offsets. A + // never-a-payload body resumes past the CLOSE; resuming past the opener + // instead would rescan the interior, and since the marker scan runs on the + // blanked body, a tag quoted inside a JSON string is invisible to it and + // would be re-parsed as a real tag on the second pass — then dropped, + // deleting the very text this parser exists to preserve. + const raw = + 'A {"a":"{\\"k\\":{\\"title\\":\\"x\\",\\"description\\":\\"y\\"}}"} junk B' + const { segments } = parseSpecialTags(raw, false) + expect(segments.every((segment) => segment.type === 'text')).toBe(true) + expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( + raw + ) + }) + it('keeps prose a tag wrapped instead of a payload', () => { // Verbatim from a real message (trace 1206fd8a): a matched pair whose body // is plain prose, never an attempted JSON payload. The sentence read @@ -319,6 +335,14 @@ describe('parseSpecialTags with ', () => { expect(parseSpecialTags(raw, true).hasPendingTag).toBe(true) }) + it('does not let an escaped quote end a string early and skew the depth', () => { + // If `\"` were read as the closing quote, the following `}` would count as a + // real close, the top-level value would look finished, and the trailing text + // would settle the tag as unresolvable mid-payload. + const raw = 'x {"title":"a \\" } b","path":"files/a.md"' + expect(parseSpecialTags(raw, true).hasPendingTag).toBe(true) + }) + it('still suppresses a JSON-bodied tag that is genuinely mid-stream', () => { const { segments, hasPendingTag } = parseSpecialTags( 'Here you go {"type":"file","id":"abc"', @@ -328,12 +352,13 @@ describe('parseSpecialTags with ', () => { expect(segments).toEqual([{ type: 'text', content: 'Here you go ' }]) }) - it('bails when a foreign closing tag appears inside the body', () => { + it('bails when a foreign closing tag appears inside a prose body', () => { // Tags never nest, so a close for a different tag proves the opener was text. - // The array is left UNCLOSED on purpose: with a closed value the JSON rule - // would independently reject the trailing content, and this test would still - // pass with the nesting rule deleted. Unclosed, only nesting can explain it. - const { hasPendingTag } = parseSpecialTags('see [{"title":"a" more', true) + // Asserted on `thinking` because that is the only tag the nesting rule still + // serves: a JSON body has no need of it, since a marker outside a string + // literal is content the viability rule already rejects, and one inside is + // legitimate quoted syntax that must not count as evidence. + const { hasPendingTag } = parseSpecialTags('see weighing it more', true) expect(hasPendingTag).toBe(false) }) @@ -353,13 +378,6 @@ describe('parseSpecialTags with ', () => { expect(segments.some((s) => s.type === 'question')).toBe(true) }) - it('still bails on a marker outside the JSON strings', () => { - // Escapes must not end the string early, and a marker in real body position - // is still evidence. - const streaming = 'ok [{"prompt":"a \\" quote"} ' - expect(parseSpecialTags(streaming, true).hasPendingTag).toBe(false) - }) - it('rejects an opener a nested one disproves, then judges the inner on its own', () => { // Each opener is evaluated independently. The first is disproved by the // nested opener and its text is released immediately; the second is a fresh diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 9c57cae7b39..312428c57dc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -583,14 +583,24 @@ function isViableJsonPrefixOf(scannable: string): boolean { * * Without this, a message that merely mentions a tag in prose goes blank from * that point on for the rest of the stream — the text is only restored once - * streaming stops. Two facts let us decide early: + * streaming stops. * - * 1. Tags never nest. Any other special-tag marker inside the body — opening or - * closing — means this opener was literal text, not a real tag. - * 2. A JSON body must stay a viable JSON prefix. Depth is tracked rather than - * testing the first character alone, so a body whose top-level value has - * already closed is caught the moment stray content follows it — including a - * misspelled close like `` instead of ``, which no marker rule sees. + * One rule decides it, chosen by body kind: + * + * - **JSON-bodied tags** must stay a viable JSON prefix. Depth is tracked rather + * than testing the first character alone, so a body whose top-level value has + * already closed is caught the moment stray content follows it — a mention in + * prose (no `{` at all), a misspelled close like ``, a + * truncated ``) - // For a JSON-bodied tag, ignore markers inside string literals: quoting tag - // syntax is legitimate content, and treating it as evidence would bail on a - // tag that goes on to close correctly — showing raw JSON that then snaps into - // a rendered card. - const isJsonBodied = JSON_BODY_TAG_NAMES.has(tagName) - const scannable = isJsonBodied ? blankJsonStringLiterals(pending) : pending - for (const name of SPECIAL_TAG_NAMES) { - // A close for this tag is absent by definition here, so this catches a - // FOREIGN close; the open check catches nesting, including self-nesting. - if (scannable.includes(``) || scannable.includes(`<${name}>`)) return true - } - if (isJsonBodied && !isViableJsonPrefixOf(scannable)) return true + if (!JSON_BODY_TAG_NAMES.has(tagName)) { + return SPECIAL_TAG_NAMES.some( + (name) => pending.includes(``) || pending.includes(`<${name}>`) + ) + } - return false + // Blank string literals first so braces and brackets inside JSON strings do + // not throw off the depth count. + return !isViableJsonPrefixOf(blankJsonStringLiterals(pending)) } /** From 39d760682464dfb619c58b1e1c3a7bbe56d93820 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:05:57 -0700 Subject: [PATCH 11/27] fix(copilot): stop the rescan deleting quoted text, and bound its cost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found the foreign-markers rescan reintroducing the failure this branch exists to remove. Three fixes, each with a mutation-checked regression test. The rescan decides on the BLANKED body but resumes at the opener and re-scans the RAW one, so a tag quoted inside a JSON string — invisible to the decision — is re-parsed as a real tag on the second pass and dropped. A question whose prompt quotes a example rendered with the quoted payload deleted. Resolution now resumes at the offset of the marker that actually survived blanking, so the quoted region is skipped and emitted verbatim. Promotion of a quoted tag into a live control is not reachable: quotes inside the outer JSON string must be escaped, so the inner body never survives JSON.parse. That offset is taken from the blanked copy and applied to the raw body, which makes index preservation load-bearing rather than decorative. A blanked astral character emitted one space for two UTF-16 units, shifting every later offset left; it now emits char.length. No test pins this — the drift only moves a text segment boundary, and adjacent text segments concatenate — so the invariant is stated in the docstring instead of guarded by a test that cannot fail. The matched-pair body had no size cap, unlike the unclosed path. A borrowed close stretches one body across most of the message, and the scan reruns for every opener inside it on every streamed chunk. A 58KB reply of that shape — organic, it is trace b095e080's own mechanism — cost 242ms per parse on the main thread, and the parse reruns per chunk. Bounding the scan to MAX_UNCLOSED_BODY_SCAN brings it to 35ms. The bound needed a guard the reviewers' version omitted: when only a prefix was inspected, finding no reason is not evidence the body was a payload, so it resolves to literal rather than discard. Without that, capping converts unexamined bodies into silent deletions — the naive fix would have caused the bug it was meant to prevent. Finally, pushText dropped whitespace-only spans. Harmless when failed tags were dropped whole; the literal path emits a rejected span in pieces, so a blank line between two of them vanished and two markdown paragraphs merged. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 38 +++++++++++ .../components/special-tags/special-tags.tsx | 64 ++++++++++++++----- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 677fd0fe5a5..c02059bf8f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -299,6 +299,44 @@ describe('parseSpecialTags with ', () => { ) }) + it('does not delete tag syntax quoted inside the body it rescans', () => { + // The rescan decides on the BLANKED body, so a tag quoted inside a JSON + // string is invisible to it. Resuming at the opener would re-scan that + // quoted text raw, re-parse it as a real tag, and drop it — deleting text. + // Resuming at the MARKER skips the quoted region, so it survives verbatim. + const inner = + '{\\"type\\":\\"link\\",\\"value\\":\\"https://x.example/p\\"}' + const raw = `A {"prompt":"${inner}"} B` + const { segments } = parseSpecialTags(raw, false) + expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( + raw + ) + expect(segments.every((segment) => segment.type === 'text')).toBe(true) + }) + + it('keeps the blank line between two rejected spans', () => { + // The renderer concatenates adjacent text segments into one markdown string, + // so a dropped whitespace-only span silently merges two paragraphs. + const raw = + 'prose one\n\nprose two' + const { segments } = parseSpecialTags(raw, false) + expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( + raw + ) + }) + + it('shows an oversized body it only partly inspected rather than discarding it', () => { + // Only the first MAX_UNCLOSED_BODY_SCAN characters are scanned. Finding no + // reason within that window is not evidence the body was a real payload, so + // the span must be shown — discarding would delete text never examined. + const body = `{"type":"file","path":"a.md","note":"${'x'.repeat(5000)}` + const raw = `see ${body} end` + const { segments } = parseSpecialTags(raw, false) + expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( + raw + ) + }) + it('still renders a matched pair whose body IS valid', () => { const raw = 'see {"type":"file","path":"files/a.md","title":"a.md"} ok' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 312428c57dc..28af8c846f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -512,6 +512,11 @@ const MAX_UNCLOSED_BODY_SCAN = 4096 * close, so the nesting rule must not see them. Tracks escapes so a `\"` inside * a string does not end it early. Handles an unterminated trailing string, which * is the normal state mid-stream. + * + * Index preservation is load-bearing, not decorative: {@link resolveTagAt} takes an + * offset found in the blanked copy and applies it to the RAW body. Iteration is by + * code point, so a blanked astral character must emit `char.length` spaces — + * emitting one would shrink the output and shift every later offset left. */ function blankJsonStringLiterals(body: string): string { let out = '' @@ -521,7 +526,7 @@ function blankJsonStringLiterals(body: string): string { for (const char of body) { if (escaped) { escaped = false - out += ' ' + out += ' '.repeat(char.length) continue } if (char === '\\' && inString) { @@ -534,7 +539,7 @@ function blankJsonStringLiterals(body: string): string { out += '"' continue } - out += inString ? ' ' : char + out += inString ? ' '.repeat(char.length) : char } return out @@ -666,16 +671,19 @@ type TagResolution = * The two reasons resume differently, which is why they are distinguished * rather than collapsed into a boolean (see {@link resolveTagAt}). */ -type LiteralTextReason = - /** The body carries tag markers, so the close we matched belongs elsewhere. */ - | 'foreign-markers' +type LiteralTextVerdict = + /** + * The body carries a tag marker at `markerOffset` (an index into the body), so + * the close we matched belongs to a different opener. + */ + | { reason: 'foreign-markers'; markerOffset: number } /** The tag wrapped prose that was never JSON to begin with. */ - | 'never-a-payload' + | { reason: 'never-a-payload' } function literalTextReason( tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string -): LiteralTextReason | null { +): LiteralTextVerdict | null { const isJsonBodied = JSON_BODY_TAG_NAMES.has(tagName) // Markers inside a JSON string are content, not evidence — a `` may // legitimately quote tag syntax in its prompt. Scanning the raw body here @@ -683,8 +691,9 @@ function literalTextReason( // which is exactly what `discard` exists to prevent. Mirrors the same blanking // in unclosedTagCannotResolve, which judges the same body mid-stream. const scannable = isJsonBodied ? blankJsonStringLiterals(body) : body - if (TAG_SHAPED_MARKER.test(scannable)) return 'foreign-markers' - if (isJsonBodied && !isViableJsonPrefixOf(scannable)) return 'never-a-payload' + const marker = TAG_SHAPED_MARKER.exec(scannable) + if (marker) return { reason: 'foreign-markers', markerOffset: marker.index } + if (isJsonBodied && !isViableJsonPrefixOf(scannable)) return { reason: 'never-a-payload' } return null } @@ -743,16 +752,33 @@ function resolveTagAt( const parsed = parseSpecialTagData(tagName, body) if (parsed) return { outcome: 'segment', segment: parsed, resumeAt } - const reason = literalTextReason(tagName, body) - if (reason === 'foreign-markers') { + // Bound this scan the way the unclosed path is bounded: a borrowed close can + // stretch one body across most of the message, and the scan then reruns for + // every opener inside it on every streamed chunk. Both rules decide on first + // evidence, so a prefix reaches the same verdict for any real payload. + const truncated = body.length > MAX_UNCLOSED_BODY_SCAN + const verdict = literalTextReason( + tagName, + truncated ? body.slice(0, MAX_UNCLOSED_BODY_SCAN) : body + ) + + if (verdict?.reason === 'foreign-markers') { // A marker in the body proves the close we matched opened somewhere else — // this opener reached past its own missing close and borrowed a later tag's. - // Resuming past the OPENER instead of past that borrowed close re-scans the - // interior, so the genuine tag inside still renders instead of being - // swallowed into one literal span. - return { outcome: 'literal', resumeAt: bodyStart } + // Resume at that marker rather than past the borrowed close, so a genuine tag + // after it still renders instead of being swallowed into one literal span. + // Resuming at the MARKER and not at the opener matters: the scan ran on the + // blanked body, so anything before the marker may be a quoted string whose + // tag syntax the scan never saw. Re-scanning that raw would re-parse the + // quoted text as a real tag and drop it — deleting the very text this parser + // exists to preserve. Everything skipped is still emitted by pushText. + return { outcome: 'literal', resumeAt: bodyStart + verdict.markerOffset } } - if (reason === 'never-a-payload') return { outcome: 'literal', resumeAt } + if (verdict?.reason === 'never-a-payload') return { outcome: 'literal', resumeAt } + + // Only a prefix was inspected, so "no reason found" is not evidence the body was + // a real payload — discarding here would delete text that was never examined. + if (truncated) return { outcome: 'literal', resumeAt } // A well-formed value that failed its shape guard is a broken emission from // the agent; showing the user raw JSON there would be worse than nothing. @@ -774,8 +800,12 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS let hasPendingTag = false let cursor = 0 + // Whitespace-only spans are kept, not trimmed away: the literal path emits a + // rejected span in several pieces, and a `\n\n` between two of them is a + // markdown paragraph break. Dropping it silently merges two paragraphs, because + // the renderer concatenates adjacent text segments into one markdown string. const pushText = (text: string) => { - if (text.trim()) segments.push({ type: 'text', content: text }) + if (text) segments.push({ type: 'text', content: text }) } const openerCache = new Map() From d2fd114889ee23aaa2d9650ccfb59b67c2b3a484 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:26:26 -0700 Subject: [PATCH 12/27] test(copilot): assert what a reader sees, and pin the unclosed-thinking trade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found several tests asserting only `hasPendingTag`. That boolean can be correct while a wrong resumeAt drops the trailing prose — the exact defect twice fixed on this branch — so those tests could not have caught it. They now assert the rendered text alongside the flag. Adds renderedText(), since a dozen tests hand-rolled the same map/join, and switches two discard tests off exact segment-array equality onto it. How a span splits across adjacent text segments is not observable: the renderer concatenates them. Pinning the array shape only breaks on refactors that change nothing a reader sees, which is what those two did. Adds a frame-replay test. Everything else asserts one end state, so nothing covered behavior BETWEEN streamed frames, where a card could render and then revert. replayFrames() parses every growing prefix and asserts card count never decreases — appending can only add closes after the ones already matched, so no earlier opener's resolution can change. Pins the unclosed- behavior as a deliberate trade rather than leaving it incidental: hidden while streaming, shown once the stream ends. Hiding is the right mid-stream default because a close is still plausible and `thinking` renders as nothing anyway. Showing it at the end does leak reasoning when the close never arrives, which is accepted — that is rare, and keeping it hidden would swallow the answer whenever the model opened the tag and then wrote the reply without closing it. Not taken: the reviewer suggestion to swap the prose path's tag-name scan for TAG_SHAPED_MARKER. It matches any ``, and thinking bodies are prose that legitimately discuss markup, so it would release those bodies early and make the leak above more frequent — the opposite of the default just chosen. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 170 ++++++++++++------ 1 file changed, 113 insertions(+), 57 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index c02059bf8f0..a4d5be57fbe 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -14,11 +14,47 @@ vi.mock('@/lib/auth/auth-client', () => ({ useSession: vi.fn(() => ({ data: null, isPending: false })), })) +import type { ContentSegment } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' import { parseQuestionTagBody, parseSpecialTags, } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' +/** + * What a reader actually sees: the renderer concatenates adjacent text segments + * into one markdown string, so how a span is split across segments is not + * observable. Assert on this rather than on segment-array shape. + */ +function renderedText(segments: ContentSegment[]): string { + return segments.map((segment) => ('content' in segment ? segment.content : '')).join('') +} + +/** + * Replays `content` the way it streams — one growing prefix per frame — and + * returns every frame's parse. A parser bug that only shows up between frames + * (a card that appears then reverts to text, prose that renders then vanishes) + * is invisible to a single end-state assertion. + */ +function replayFrames(content: string, step = 1) { + const frames: { text: string; cardCount: number }[] = [] + for (let end = 1; end <= content.length; end += step) { + const { segments } = parseSpecialTags(content.slice(0, end), true) + frames.push({ + text: renderedText(segments), + cardCount: segments.filter( + (segment) => segment.type !== 'text' && segment.type !== 'thinking' + ).length, + }) + } + const { segments } = parseSpecialTags(content, false) + frames.push({ + text: renderedText(segments), + cardCount: segments.filter((segment) => segment.type !== 'text' && segment.type !== 'thinking') + .length, + }) + return frames +} + const SINGLE_SELECT = { type: 'single_select', prompt: 'How should I handle the duplicate emails?', @@ -163,9 +199,7 @@ describe('parseSpecialTags with ', () => { 'recognize it as a valid resource chip. A properly matched pair ' + '(`...`) is what actually produces the interactive chip.' - const rendered = parseSpecialTags(raw, false) - .segments.map((segment) => ('content' in segment ? segment.content : '')) - .join('') + const rendered = renderedText(parseSpecialTags(raw, false).segments) expect(rendered).toContain("Since the closing tag doesn't match") expect(rendered).toContain('A properly matched pair') @@ -193,13 +227,10 @@ describe('parseSpecialTags with ', () => { // complete, every character survives. const raw = 'The dataset lives in {"type": "file", "path": "files/notes.md"} and I keep coming back to it whenever I need a quick reference. It never quite has everything.' - const join = (result: ReturnType) => - result.segments.map((segment) => ('content' in segment ? segment.content : '')).join('') - const streaming = parseSpecialTags(raw, true) expect(streaming.hasPendingTag).toBe(false) - expect(join(streaming)).toBe(raw) - expect(join(parseSpecialTags(raw, false))).toBe(raw) + expect(renderedText(streaming.segments)).toBe(raw) + expect(renderedText(parseSpecialTags(raw, false).segments)).toBe(raw) }) it('does not rescan the interior of a body that carried no markers', () => { @@ -213,9 +244,7 @@ describe('parseSpecialTags with ', () => { 'A {"a":"{\\"k\\":{\\"title\\":\\"x\\",\\"description\\":\\"y\\"}}"} junk B' const { segments } = parseSpecialTags(raw, false) expect(segments.every((segment) => segment.type === 'text')).toBe(true) - expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( - raw - ) + expect(renderedText(segments)).toBe(raw) }) it('keeps prose a tag wrapped instead of a payload', () => { @@ -224,9 +253,7 @@ describe('parseSpecialTags with ', () => { // "...once I wired up to handle the welcome sequence" with the subject gone. const raw = 'once I wired up the gmail-agent workflow to handle the welcome sequence.' - const rendered = parseSpecialTags(raw, false) - .segments.map((segment) => ('content' in segment ? segment.content : '')) - .join('') + const rendered = renderedText(parseSpecialTags(raw, false).segments) expect(rendered).toContain('the gmail-agent workflow') expect(rendered).toContain('to handle the welcome sequence') }) @@ -239,10 +266,11 @@ describe('parseSpecialTags with ', () => { false ) expect(hasPendingTag).toBe(false) - expect(segments).toEqual([ - { type: 'text', content: 'Before. ' }, - { type: 'text', content: ' After.' }, - ]) + // Asserted on the rendered text, not the segment array: how the surviving + // prose is split across text segments is display-neutral, so pinning the + // array shape would break on a behavior-preserving change to the split. + expect(renderedText(segments)).toBe('Before. After.') + expect(segments.every((segment) => segment.type === 'text')).toBe(true) }) it('drops that same payload even when its JSON quotes tag syntax', () => { @@ -254,10 +282,8 @@ describe('parseSpecialTags with ', () => { 'A [{"type":"single_select","prompt":"use here?"}] B', false ) - expect(segments).toEqual([ - { type: 'text', content: 'A ' }, - { type: 'text', content: ' B' }, - ]) + expect(renderedText(segments)).toBe('A B') + expect(segments.every((segment) => segment.type === 'text')).toBe(true) }) it('does not flash the payload while the closing tag is still arriving', () => { @@ -270,7 +296,7 @@ describe('parseSpecialTags with ', () => { true ) expect(hasPendingTag).toBe(true) - expect(segments.map((s) => ('content' in s ? s.content : '')).join('')).toBe('see ') + expect(renderedText(segments)).toBe('see ') } }) @@ -278,11 +304,14 @@ describe('parseSpecialTags with ', () => { // The counterpart to the case above: `` can never grow // into ``, so it settles immediately instead of hiding // the rest of the message for the remainder of the stream. - const { hasPendingTag } = parseSpecialTags( - 'see {"type":"file","path":"a.md"} and then prose.', - true - ) + const raw = + 'see {"type":"file","path":"a.md"} and then prose.' + const { hasPendingTag, segments } = parseSpecialTags(raw, true) expect(hasPendingTag).toBe(false) + // Asserted on the text too, not just the flag: a wrong resumeAt keeps the + // flag correct while dropping the prose, which is the defect class this + // whole change exists to prevent. + expect(renderedText(segments)).toBe(raw) }) it('keeps a valid tag whose close an earlier broken tag would borrow', () => { @@ -294,9 +323,7 @@ describe('parseSpecialTags with ', () => { 'and a real one: {"type":"file","path":"b.md","title":"b"}' const { segments } = parseSpecialTags(raw, false) expect(segments.some((s) => s.type === 'workspace_resource')).toBe(true) - expect(segments.map((s) => ('content' in s ? s.content : '')).join('')).toContain( - '' - ) + expect(renderedText(segments)).toContain('') }) it('does not delete tag syntax quoted inside the body it rescans', () => { @@ -308,9 +335,7 @@ describe('parseSpecialTags with ', () => { '{\\"type\\":\\"link\\",\\"value\\":\\"https://x.example/p\\"}' const raw = `A {"prompt":"${inner}"} B` const { segments } = parseSpecialTags(raw, false) - expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( - raw - ) + expect(renderedText(segments)).toBe(raw) expect(segments.every((segment) => segment.type === 'text')).toBe(true) }) @@ -320,9 +345,7 @@ describe('parseSpecialTags with ', () => { const raw = 'prose one\n\nprose two' const { segments } = parseSpecialTags(raw, false) - expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( - raw - ) + expect(renderedText(segments)).toBe(raw) }) it('shows an oversized body it only partly inspected rather than discarding it', () => { @@ -332,9 +355,7 @@ describe('parseSpecialTags with ', () => { const body = `{"type":"file","path":"a.md","note":"${'x'.repeat(5000)}` const raw = `see ${body} end` const { segments } = parseSpecialTags(raw, false) - expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( - raw - ) + expect(renderedText(segments)).toBe(raw) }) it('still renders a matched pair whose body IS valid', () => { @@ -363,9 +384,7 @@ describe('parseSpecialTags with ', () => { 'kicks off in {"type":"file","path":"files/notes.md"} ('content' in s ? s.content : '')).join('')).toContain( - 'I brew a cup of coffee' - ) + expect(renderedText(segments)).toContain('I brew a cup of coffee') }) it('tolerates braces inside JSON strings when tracking depth', () => { @@ -396,8 +415,10 @@ describe('parseSpecialTags with ', () => { // serves: a JSON body has no need of it, since a marker outside a string // literal is content the viability rule already rejects, and one inside is // legitimate quoted syntax that must not count as evidence. - const { hasPendingTag } = parseSpecialTags('see weighing it more', true) + const raw = 'see weighing it more' + const { hasPendingTag, segments } = parseSpecialTags(raw, true) expect(hasPendingTag).toBe(false) + expect(renderedText(segments)).toBe(raw) }) it('does not bail on tag syntax quoted inside a JSON string', () => { @@ -422,23 +443,60 @@ describe('parseSpecialTags with ', () => { // candidate that nothing has ruled out yet, so it holds mid-stream. const streaming = parseSpecialTags('a b c', true) expect(streaming.hasPendingTag).toBe(true) - expect( - streaming.segments.map((segment) => ('content' in segment ? segment.content : '')).join('') - ).toBe('a b ') + expect(renderedText(streaming.segments)).toBe('a b ') // Once the stream ends nothing can close it, so the whole line is shown. const done = parseSpecialTags('a b c', false) expect(done.hasPendingTag).toBe(false) - expect( - done.segments.map((segment) => ('content' in segment ? segment.content : '')).join('') - ).toBe('a b c') + expect(renderedText(done.segments)).toBe('a b c') + }) + + it('hides an unclosed thinking body while streaming, then shows it once complete', () => { + // A DELIBERATE trade, not an oversight. `thinking` bodies are prose, so the + // JSON viability rule cannot apply and only the nesting rule can disprove the + // opener — mid-stream the default is therefore to HIDE, since a close is + // still plausible and releasing early would flash reasoning that is about to + // become a suppressed segment. + // + // Once the stream ends the body is shown as text, which does leak the model's + // reasoning for a message whose close never arrived. Accepted: forgetting the + // close is rare, and the alternative — keeping it hidden — would swallow the + // answer whenever the model opened `` and then wrote the reply + // without closing, which is the text-loss bug this whole change removes. + const raw = 'a still reasoning about' + const streaming = parseSpecialTags(raw, true) + expect(streaming.hasPendingTag).toBe(true) + expect(renderedText(streaming.segments)).toBe('a ') + + const complete = parseSpecialTags(raw, false) + expect(complete.hasPendingTag).toBe(false) + expect(renderedText(complete.segments)).toBe(raw) }) - it('keeps suppressing an unclosed thinking tag with prose — its body is not JSON', () => { - // Documents the deliberate gap: `thinking` bodies are prose, so the JSON - // heuristic cannot apply and only the nesting rule can rescue it. - const { hasPendingTag } = parseSpecialTags('a still reasoning about', true) - expect(hasPendingTag).toBe(true) + it('never retracts rendered text or a card across streamed frames', () => { + // Frame-to-frame stability, which no end-state assertion can see. Replays a + // real message one character at a time: text already shown must never + // disappear, and a card once rendered must never revert to raw text. + const content = + 'Updated {"type":"file","path":"files/a.md","title":"a.md"} ' + + 'and left `` alone. ' + + '[{"title":"Ship it","description":"open the PR"}]' + + const frames = replayFrames(content) + + // Card count is monotonically non-decreasing. Appending to the buffer can + // only add closes AFTER the ones already matched, so no earlier opener's + // resolution can change — a card that renders must never un-render. + let previous = 0 + for (const frame of frames) { + expect(frame.cardCount).toBeGreaterThanOrEqual(previous) + previous = frame.cardCount + } + + // The settled parse is the richest: both tags resolved, prose intact. + const settled = frames[frames.length - 1] + expect(settled.cardCount).toBe(2) + expect(settled.text).toContain('and left `` alone.') }) it('renders an unclosed tag as text once the message is complete', () => { @@ -450,9 +508,7 @@ describe('parseSpecialTags with ', () => { // concatenates adjacent text segments, so how the span is split is not // observable to a reader. expect(segments.every((segment) => segment.type === 'text')).toBe(true) - expect(segments.map((segment) => ('content' in segment ? segment.content : '')).join('')).toBe( - content - ) + expect(renderedText(segments)).toBe(content) }) it('strips a trailing partial opening tag while streaming', () => { From 0abfbc19977e187c1b29a0f563155a48ed94ba29 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:25:21 -0700 Subject: [PATCH 13/27] docs(copilot): name the scan window's blind spot instead of glossing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugbot is right that the window can hide a streaming tail, and the docstring said the bound "reaches the same verdict as the full remainder for any real payload" — true for every payload a tag carries, but it read as unconditional and hid the exception. The exception, now stated and pinned by a test: a JSON body whose top-level value closes BEYOND the window, followed by prose and no closing tag, still reads as a viable prefix, so the remainder waits for the stream to end instead of settling mid-stream. Lossless — the completed parse renders every character — and it needs a payload several times larger than any tag emits. A mention in prose settles at its first character at any length, because prose does not open with a brace; the test pins that half too, since it is the case that actually occurs. Not widening the window: the bound is what took a 58KB borrowed-close reply from 242ms to 35ms per parse, on the main thread, re-run every chunk. Trading a measured freeze for a hypothetical one is the wrong direction. The docstring also now covers the matched-pair path, which the same constant bounds since the previous commit. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 18 +++++++++++ .../components/special-tags/special-tags.tsx | 31 +++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index a4d5be57fbe..a017c2fe37c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -358,6 +358,24 @@ describe('parseSpecialTags with ', () => { expect(renderedText(segments)).toBe(raw) }) + it('settles a prose mention at any length, but defers a payload that closes past the window', () => { + // The scan window's accepted blind spot, pinned so it stays a decision. + // + // A mention in prose settles at its FIRST character however long the message + // runs — prose does not open with `{`, so viability fails immediately. + const mention = `see ${'long prose. '.repeat(600)}` + expect(parseSpecialTags(mention, true).hasPendingTag).toBe(false) + + // But a JSON body whose top-level value closes BEYOND the window still reads + // as a viable prefix, so the tail stays hidden until the stream ends. Needs a + // payload several times larger than any tag emits, and it is lossless once + // complete — the cost of bounding a scan that otherwise stalls the main + // thread. + const oversized = `see {"type":"file","note":"${'x'.repeat(5000)}"} and then prose.` + expect(parseSpecialTags(oversized, true).hasPendingTag).toBe(true) + expect(renderedText(parseSpecialTags(oversized, false).segments)).toBe(oversized) + }) + it('still renders a matched pair whose body IS valid', () => { const raw = 'see {"type":"file","path":"files/a.md","title":"a.md"} ok' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 28af8c846f8..e2e73b688ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -489,16 +489,29 @@ const JSON_BODY_TAG_NAMES: ReadonlySet<(typeof SPECIAL_TAG_NAMES)[number]> = new ) /** - * How much of an unclosed body to inspect per parse. + * How much of a body to inspect per parse, on both the unclosed and matched-pair + * paths. * - * Both rules in {@link unclosedTagCannotResolve} decide on their FIRST piece of - * evidence — the first foreign marker, or the first character that breaks JSON - * viability — so a bounded window reaches the same verdict as the full remainder - * for any real payload. Unbounded, the check is O(remaining length) and runs - * once per opener inside a parse that re-runs for every streamed chunk, so a - * long reply repeatedly mentioning a tag name in prose costs seconds of - * main-thread time. Evidence past the window only defers the decision to a later - * chunk, which is the same conservative direction the rules already take. + * The rules in {@link unclosedTagCannotResolve} and {@link literalTextReason} + * decide on their FIRST piece of evidence — the first foreign marker, or the + * first character that breaks JSON viability — so a bounded window reaches the + * same verdict as the full remainder for any payload a tag actually carries. + * Unbounded, the check is O(body length) and runs once per opener inside a parse + * that re-runs for every streamed chunk: a long reply repeatedly mentioning a tag + * name cost seconds of main-thread time, and one 58KB reply whose early close was + * misspelled — so a single body stretched most of the message — cost 242ms per + * parse against 35ms bounded. + * + * The window's one blind spot, and why it is accepted: a JSON body whose + * top-level value closes BEYOND the window, followed by prose and no closing tag, + * still reads as a viable prefix, so the remainder stays hidden until the stream + * ends rather than settling mid-stream. It is lossless — the completed parse + * renders every character — and it needs a payload several times larger than any + * tag emits (a `` runs ~100 characters, a `` card + * under ~1500). A mention in prose settles at its first character at any length, + * because prose does not open with `{`. Widening or removing the window to close + * that gap would trade a measured, reachable main-thread freeze for a + * hypothetical one. */ const MAX_UNCLOSED_BODY_SCAN = 4096 From d6db73d91564a51daeee8a14c650d2152bc3dfa4 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:32:35 -0700 Subject: [PATCH 14/27] fix(copilot): make the indexOf cache correct for any call order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cache was safe only under a precondition the code could not enforce: that `from` never decreases within a parse. Violating it does not throw — a stale index is returned and the parser silently mis-parses, which is the exact failure class this branch exists to remove. Each entry now records the offset it was searched at, and is reused only when the new `from` is at or beyond it. A cached -1 still answers any later `from`, since absence from an earlier offset implies absence from a later one; a cached hit still answers when it lies ahead of `from`. Anything else rescans. The precondition held and still holds — every non-pending outcome resumes strictly past its opener. But it is a property of resolveTagAt's resume points, not of this function, and one of those points deliberately resumes back inside a span it already examined. Someone adjusting a resume point should pay a redundant scan, not corrupt a parse. Verified by mutation: reverting the guard fails the new backward-cursor test and nothing else, so the guard is load-bearing and the monotonic path is unchanged. Same-content benchmark on a 79KB reply: 3.13 ms/frame before, 2.96 after — the entry object is allocated only on a real scan, which is already bounded. memoizedIndexOf is exported for the test. The invariant cannot be provoked through parseSpecialTags, because the cursor does not walk backward today; that is the whole point, so it has to be exercised directly. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 49 ++++++++++++++++++- .../components/special-tags/special-tags.tsx | 43 ++++++++++++---- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index a017c2fe37c..e69e5d96e7f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -14,8 +14,12 @@ vi.mock('@/lib/auth/auth-client', () => ({ useSession: vi.fn(() => ({ data: null, isPending: false })), })) -import type { ContentSegment } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' +import type { + ContentSegment, + IndexOfCache, +} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' import { + memoizedIndexOf, parseQuestionTagBody, parseSpecialTags, } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags' @@ -619,3 +623,46 @@ describe('service_account tag validation', () => { } ) }) + +describe('memoizedIndexOf', () => { + const CONTENT = + 'Use for files. Use for cards. ' + + '[{"title":"Ship","description":"go"}] and again.' + const NEEDLES = ['', '', '', ''] + + it('matches plain indexOf as the cursor advances', () => { + const cache: IndexOfCache = new Map() + for (let from = 0; from <= CONTENT.length; from++) { + for (const needle of NEEDLES) { + expect(memoizedIndexOf(cache, CONTENT, needle, from)).toBe(CONTENT.indexOf(needle, from)) + } + } + }) + + it('stays correct when the cursor moves BACKWARD', () => { + // The cache is only reused when the new `from` is at or beyond the offset the + // entry was searched at. Without that guard a cached hit — or a cached -1 — + // is returned for a region it never examined, and the parser silently + // mis-parses rather than failing loudly. + // + // parseSpecialTags never walks backward today, so this cannot be provoked + // through the public API; the point is that a future change to a resume point + // costs a redundant scan instead of a wrong answer. + const cache: IndexOfCache = new Map() + const offsets = [70, 5, 100, 0, 45, 62, 12, CONTENT.length, 40, 3] + for (const from of offsets) { + for (const needle of NEEDLES) { + expect(memoizedIndexOf(cache, CONTENT, needle, from)).toBe(CONTENT.indexOf(needle, from)) + } + } + }) + + it('caches an absent needle instead of rescanning', () => { + const cache: IndexOfCache = new Map() + expect(memoizedIndexOf(cache, CONTENT, '', 0)).toBe(-1) + // Same offset or later: answerable from the entry, since absence from 0 + // implies absence from anywhere after it. + expect(memoizedIndexOf(cache, CONTENT, '', 30)).toBe(-1) + expect(cache.get('')).toEqual({ idx: -1, from: 0 }) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index e2e73b688ae..3d0b326511b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -710,6 +710,16 @@ function literalTextReason( return null } +/** One memoized `indexOf` result, with the `from` it was computed at. */ +interface IndexOfCacheEntry { + /** Result of `content.indexOf(needle, from)`, or -1 when absent from that point on. */ + idx: number + /** The offset the search started at. The entry says nothing about content before it. */ + from: number +} + +export type IndexOfCache = Map + /** * `content.indexOf(needle, from)` memoized per needle. * @@ -720,19 +730,32 @@ function literalTextReason( * the buffer for every opener, which is quadratic on a message that mentions a * tag name many times — and this parse re-runs for every streamed chunk. * - * Safe because `from` only ever increases within a parse: a cached -1 stays -1, - * and a cached hit at or after `from` is still the first hit at or after `from`. + * A cached result is only valid from the offset it was searched at, so the entry + * carries that offset and is reused only when the new `from` is at or beyond it: + * + * - `idx === -1` means no hit at or after `entry.from`, so there is none at or + * after any later `from` either. + * - `idx >= from` means the first hit at or after `entry.from` is still ahead of + * `from`, so nothing lies between them and it is still the first hit. + * + * Storing `from` is what makes this correct for ANY call order rather than only + * for a monotonically advancing cursor. The cursor is monotonic today — every + * non-pending outcome resumes strictly past its opener — but that is a property + * of {@link resolveTagAt}'s resume points, and one of them deliberately resumes + * back inside a span it already examined. A future adjustment that let the cursor + * regress would, without this check, return a stale index and silently mis-parse + * rather than fail loudly. With it, the worst case is a redundant scan. */ -function memoizedIndexOf( - cache: Map, +export function memoizedIndexOf( + cache: IndexOfCache, content: string, needle: string, from: number ): number { - const cached = cache.get(needle) - if (cached !== undefined && (cached === -1 || cached >= from)) return cached + const entry = cache.get(needle) + if (entry && from >= entry.from && (entry.idx === -1 || entry.idx >= from)) return entry.idx const idx = content.indexOf(needle, from) - cache.set(needle, idx) + cache.set(needle, { idx, from }) return idx } @@ -741,7 +764,7 @@ function resolveTagAt( openIndex: number, tagName: (typeof SPECIAL_TAG_NAMES)[number], isStreaming: boolean, - closeCache: Map + closeCache: IndexOfCache ): TagResolution { const openTag = `<${tagName}>` const closeTag = `` @@ -821,8 +844,8 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS if (text) segments.push({ type: 'text', content: text }) } - const openerCache = new Map() - const closeCache = new Map() + const openerCache: IndexOfCache = new Map() + const closeCache: IndexOfCache = new Map() while (cursor < content.length) { let nearestStart = -1 From 737aada25c027abf33671551c2b036f5c7191cb1 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:51:01 -0700 Subject: [PATCH 15/27] fix(copilot): resume at the window edge, not past the close, on a truncated body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression from the scan bound added two commits ago. When a borrowed close stretches a body past MAX_UNCLOSED_BODY_SCAN and the inspected prefix is marker-free prose, resolution resumed past the borrowed close — skipping the uninspected remainder entirely. A valid tag sitting in that remainder was flattened to plain text purely because of which side of the window it landed on. Measured: with ~1KB of prose before it the chip rendered, with ~6KB it did not. A verdict drawn from a prefix — "prose", or no verdict at all — says nothing about the rest of the body, so resumption now lands on the first character that was NOT inspected. Everything inspected is still emitted as text by the caller, and scanning continues into the remainder, so the tag renders. Ordering matters: foreign-markers keeps priority, since a marker found inside the window is a real offset and resuming there is strictly earlier and safer. No text was ever lost in this case — it stayed lossless throughout — but a destroyed chip is a real regression, and it was introduced by the fix for the previous one. Cost is bounded: each truncated step advances a full window, so a long body costs body-length/window re-entries rather than one scan per character. A 117KB borrowed body parses in 0.1ms. Verified by mutation: restoring the old resume point fails the new test at 6KB and 60KB and nothing else. The test asserts three lengths so the boundary itself is covered rather than one side of it. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 24 +++++++++++++++++++ .../components/special-tags/special-tags.tsx | 15 ++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index e69e5d96e7f..ecb6a761007 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -380,6 +380,30 @@ describe('parseSpecialTags with ', () => { expect(renderedText(parseSpecialTags(oversized, false).segments)).toBe(oversized) }) + it('still finds a valid tag sitting past the scan window inside a borrowed body', () => { + // The first opener has no close of its own, so it borrows the inner tag's. + // Its body is marker-free prose for far longer than the scan window, so the + // truncated inspection sees only prose and can say nothing about the rest. + // + // Resuming past the borrowed close would flatten the inner tag to text purely + // because of where it fell relative to the window. Resuming at the first + // uninspected character finds it. Asserted at three lengths so the boundary + // itself is covered, not just one side of it. + const inner = + '{"type":"file","path":"files/b.md","title":"b.md"}' + const build = (proseChars: number) => + `See ${'prose word '.repeat(Math.ceil(proseChars / 11))}${inner} end` + + for (const proseChars of [1_000, 6_000, 60_000]) { + const raw = build(proseChars) + const { segments } = parseSpecialTags(raw, false) + expect(segments.filter((segment) => segment.type === 'workspace_resource')).toHaveLength(1) + // The prose around it survives too — the span is emitted, not skipped. + expect(renderedText(segments)).toContain('See prose word') + expect(renderedText(segments)).toContain(' end') + } + }) + it('still renders a matched pair whose body IS valid', () => { const raw = 'see {"type":"file","path":"files/a.md","title":"a.md"} ok' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 3d0b326511b..61cf072786f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -810,11 +810,18 @@ function resolveTagAt( // exists to preserve. Everything skipped is still emitted by pushText. return { outcome: 'literal', resumeAt: bodyStart + verdict.markerOffset } } - if (verdict?.reason === 'never-a-payload') return { outcome: 'literal', resumeAt } + // Only a prefix was inspected, so no verdict drawn from it — "prose" or nothing + // at all — describes the rest of the body. Resume at the first character NOT + // looked at rather than past the close: everything inspected is emitted as text + // by the caller, and scanning continues into the remainder, so a genuine tag + // beyond the window still renders as a card instead of being flattened. Past + // the close would also risk discarding a region never examined. + // + // This advances at least MAX_UNCLOSED_BODY_SCAN per step, so a long body costs a + // bounded number of re-entries rather than one scan per character. + if (truncated) return { outcome: 'literal', resumeAt: bodyStart + MAX_UNCLOSED_BODY_SCAN } - // Only a prefix was inspected, so "no reason found" is not evidence the body was - // a real payload — discarding here would delete text that was never examined. - if (truncated) return { outcome: 'literal', resumeAt } + if (verdict?.reason === 'never-a-payload') return { outcome: 'literal', resumeAt } // A well-formed value that failed its shape guard is a broken emission from // the agent; showing the user raw JSON there would be worse than nothing. From cc7ebda0690f9e2ff11a3fd48575d2ff63e9d41e Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:09:16 -0700 Subject: [PATCH 16/27] fix(copilot): only discard a body that actually parsed as JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `discard` fired on any body parseSpecialTagData rejected, which conflated "is not valid JSON" with "is valid JSON of the wrong shape" — even though its own comment has always said "a well-formed value that failed its shape guard." The viability rule cannot separate them, because bracket depth reads `{the Q4 report}` as a viable JSON value. So prose someone wrapped in braces was deleted: I saved {the Q4 report} for you. -> I saved for you. along with the two commonest JSON slips a model makes, unquoted keys (`{type: "file"}`) and single quotes (`{'type':'file'}`). All three are text loss of exactly the kind this branch exists to remove. Dropping text is only defensible for a payload the agent actually formed, so discard now requires the body to parse. Anything that will not parse was never demonstrably a payload and is shown instead. A valid payload with the wrong shape still discards, which is the case the outcome was written for, and a valid tag still renders. Costs a second JSON.parse of a body that already failed one. That is the rare path: a valid payload returns before it, and prose is rejected by the cheaper viability rule before it. Verified by mutation: removing the gate fails the new test and nothing else. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 17 +++++++++++ .../components/special-tags/special-tags.tsx | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index ecb6a761007..0adde968176 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -262,6 +262,23 @@ describe('parseSpecialTags with ', () => { expect(rendered).toContain('to handle the welcome sequence') }) + it('shows a body that will not parse at all, rather than dropping it', () => { + // `discard` is only defensible for a payload the agent actually FORMED — + // valid JSON that failed its shape guard. Bracket depth cannot tell prose + // wrapped in braces from a real payload, so without an actual parse these + // were deleted: the first is a resource name someone wrote in braces, the + // other two are the commonest JSON slips a model makes. + const cases = [ + 'I saved {the Q4 report} for you.', + 'See {type: "file", path: "a.md"} ok', + "See {'type':'file'} ok", + ] + for (const raw of cases) { + const { segments } = parseSpecialTags(raw, false) + expect(renderedText(segments)).toBe(raw) + } + }) + it('still drops a marker-free malformed payload rather than showing raw JSON', () => { // The complement of the case above: no tag markers in the body, so this is // a genuinely broken emission from the agent, not swallowed prose. diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 61cf072786f..344f87c975d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -392,6 +392,25 @@ export function parseTextTagBody(body: string): string | null { return body.trim() ? body : null } +/** + * Whether `body` is syntactically valid JSON, regardless of its shape. + * + * Separates "the agent formed a payload that failed its shape guard" from "this + * was never JSON" — the line that decides whether a failed body may be dropped + * or must be shown (see {@link resolveTagAt}). Costs a second parse of a body + * that already failed one, which is the rare path; the common cases never reach + * it, since a valid payload returns earlier and prose is rejected by the cheaper + * viability rule before this runs. + */ +function isParseableJson(body: string): boolean { + try { + JSON.parse(body) + return true + } catch { + return false + } +} + export function parseTagAttributes(openTag: string): Record { const attributes: Record = {} const attributePattern = /([A-Za-z_:][A-Za-z0-9_:-]*)="([^"]*)"/g @@ -823,6 +842,16 @@ function resolveTagAt( if (verdict?.reason === 'never-a-payload') return { outcome: 'literal', resumeAt } + // Dropping text is only defensible for a payload the agent actually FORMED: + // syntactically valid JSON that failed its shape guard. A body that will not + // parse at all was never demonstrably a payload — `{the Q4 report}` is prose + // someone wrapped in braces, and unquoted keys or single quotes are ordinary + // model slips — so it is shown rather than deleted. Bracket depth cannot tell + // those apart from a real payload; only an actual parse can. + if (JSON_BODY_TAG_NAMES.has(tagName) && !isParseableJson(body)) { + return { outcome: 'literal', resumeAt } + } + // A well-formed value that failed its shape guard is a broken emission from // the agent; showing the user raw JSON there would be worse than nothing. return { outcome: 'discard', resumeAt } From 2f9b60aa9d296c5fda6030e04b1e861127a065a8 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:24:41 -0700 Subject: [PATCH 17/27] fix(copilot): apply the nesting rule to a prose body on the matched-pair path too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prose body has no shape to fail — parseTextTagBody accepts any non-empty text — so was the one tag whose close was accepted unconditionally. The unclosed path already refuses an opener whose body carries a tag marker, on the grounds that tags never nest. The matched-pair path did not, and the two disagreeing is the bug. Mid-stream a nested marker disproves the outer opener, so its text is released and the inner tag renders. When finally arrived it was accepted as a segment, and everything already on screen was swallowed into it and suppressed: a b [...] c -> "a b [CARD] c" a b [...] c d -> "a d" A rendered card and its surrounding text disappearing from a message the reader was already looking at. Both paths now apply the same rule, and resolution resumes at the opener so the inner tag survives the rescan — safe here because prose bodies are never blanked, so no marker is hidden from the scan the way one can be inside a JSON string. The frame-replay helper counted a thinking body as visible text, which is why its card-monotonicity assertion could not see this. It now models the renderer: thinking contributes nothing. Verified by mutation — removing the rule fails the new test and nothing else. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | Bin 33389 -> 34936 bytes .../components/special-tags/special-tags.tsx | 13 +++++++++++++ 2 files changed, 13 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 0adde9681761ea9a04f6bd381e2f2a13ece144d9..c681190abc992264dedaf1ecd0fa8da9a5e9a4e7 100644 GIT binary patch delta 1612 zcmaKsO^YK{6oz%k^t4&I5r)B!)7ezgBo)Nfgmi0k@MGZ!L*pQ#&h5Gc(GR-}wIT z-u7WCJo%K#vXE>k5t>Be3z20Ta?CNkudFrJQG&=xVzg)NWADypB;<)m@Q*y7`9AF< zeznGb#2TFl1mZg;L63Z@^kb!0kGc>Xr*WLy()|yP-hJ=>I|$Q~_>}F24Dzwp^hh~~ zB*My=?kVS$PCPBGS=0F&8H8IgmPg3rVoYVKk`y@I7#UEr+M!6R$-+9JZ84 zba2?-r!Fw|o1E$3;DDm8>WE&aD54ovKlJurdgiUggL_f?+NhH~F-i}ksCspGQ~lAC zd*y#j(J+AIB#+ZNZ21td*{p0xMIO#^D`BF|aC zU+8~}6M%$PE@d~We!Y46@`D=85lWkf`pohg!1A#Aa_8;a&G~U&xO8|$w;WbK-@0A> zx%0w)+<9{ps=mPYe_o;fRru5IcJDr{K7H-EZN?s!$l&QqOa0px!S=xwmmaw6e>A3H z=vUj^>68|v*?E?<7WOgbPAZpMa|(vRPp0s=H}86QNNXjxlg4FZa)uO~9i|SWQ8y8a zmNOO(ieZp~h|(G&!NQlFxSFE{;W#+D9*7hZp$)lIz&+k{ic6ugFeFgiklq?S;dSme z&B32zMX04h!XW}Fm7&G+=~d24rRyystW}{fg>k_LsWyH(#)Ksa=Yawe5S@vyt%4x7 zYH@C{7`l17ROl4j!Gx01KH#Y-xFXLjJ3xu)2t)$%AZUw>F|0sn2=>FKgEEkHYHQ(> zl;tEB%2FK`3`U+#3g^RyfkM*{2v&>am5+{TZt^hR?(pODfmc4`!EA6g7!M?4 z3#j@T?ra7ZpUkISC?(gC$*5oF03+cP#~vcGT11nb`YNuTx074#tJRa}&Tbo0Kad^z zXf%qwd6S1rkvSeUlEHH@21=-zV%b>id0bF5XRv+$z4iCkGB@fsE=XE2p4!F_^^NkVoPdg$0+wZHU?U78D;H%%3JUMk5Hgt(HlRc?!9TEIf>VU#R(_m- zi-HpmV#B7ufVc?F-%RL_-gs|byZJy$n#M_;K1y?;d=PmOpoCIi_Am7NwWAk<75&y< z-?qWLdL8nzoFZplSD>-dth;gO4%` that had + // been shown as text with a rendered card inside it would blank on that frame, + // taking the card with it. Resuming at the opener rescans the interior, so the + // inner tag survives; prose bodies are never blanked, so no marker is hidden + // from this scan the way one can be in a JSON string. + if (!JSON_BODY_TAG_NAMES.has(tagName) && TAG_SHAPED_MARKER.test(body)) { + return { outcome: 'literal', resumeAt: bodyStart } + } + const parsed = parseSpecialTagData(tagName, body) if (parsed) return { outcome: 'segment', segment: parsed, resumeAt } From 3d429e366715d598fb3919d842874a79b591d222 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:41:40 -0700 Subject: [PATCH 18/27] test(copilot): assert parser invariants over generated messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every regression review found on this branch was a combination nobody had written an example for. The example tests cannot cover the space: a body is judged on body kind, close state, JSON state, marker placement, size against the scan window, and streaming mode — a product of roughly six hundred cells, each needing both an outcome and a resume point. Four invariants over messages composed from fragments, so a new combination is covered without a new test: - no character is lost from a message containing nothing droppable - every valid tag renders as a card whatever surrounds it - across streamed frames a card never un-renders and text never retracts - the settled parse never shows less than the last streaming frame Fragments are the shapes the parser must reject — prose mentions, misspelled and truncated closes, brace-wrapped prose, unquoted keys, a nested marker in a prose body, filler long enough to cross the scan window. None is a valid tag or a well-formed payload, so nothing is eligible for discard, which is what makes "output equals input" a legal assertion. Seeded, so a failure reproduces. Validated against the three defects review found, with the fix for each reverted and only these tests running: the flattened tag past the window fails the card invariant, the deleted brace-wrapped prose fails the loss invariant, and the retracted thinking close fails both loss and retraction. They pass on current code, so nothing further is reachable through the shapes they generate. The generator composes several fragments between tags rather than one. With a single fragment it cannot place an unclosed opener and then enough prose to push a valid tag past the scan window, which is exactly the shape of one of the three — the first draft missed it for that reason alone. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | Bin 34936 -> 41628 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index c681190abc992264dedaf1ecd0fa8da9a5e9a4e7..b7cdfe42add641ef10bccb6672359323325e45cb 100644 GIT binary patch delta 5569 zcmbVQO>-Mr6)h%3rQ%{SK*%r*A-6(Ok{nt7$QSY0E==MCn2eK(T|iY5qNnaBb;qsl z&g<8fg>iMJvSCTltSA=z1pWlOSyN;245MAxRD@7&w2-w&19(}rE95gZM5Y}=HD zztN-yDvUFWfUpo-(+Hl5WYi`V`&e$Y8iw(JhKaA~AJ6}Y5K@!)xo0WqvqO{QgCH8i z+W`f6>>EVoCBt4AD;p+p)^^8_wf5OhFR=k!Q<7U)VSntR$mlE|YKboiV_S4+3#lIC z`vD$h@I(Cs&Iw60dOy@7$`a};BRN#$=_sN&k>mu*r;!P*)iJ!Y&$=f4#LScmnNnGX z>cZ*1GNFo5Va|nC1#&5=uPe+aN|9Ti`X(9Tj~Z|eaX5sbc#jw%%^aIcB-O)`QD_4k z7sq5H?xYUwARTzbt8t7l%8kasTxNAvaF~m8zRo-o_H?t6DuYNl^W2W1&KhlMwrG8W z4(Eu_tTfD{o;GHDc}1$^YpV?k7iKo}T$Hj8m%vjvI^wJM3w!8SIai1bWQYv;$M!`1 zJodPVG*o-~K|E;k!&Qp&A@*vmRw1~L5(!bH`Jn6KhBh`fXbFA@d)eH?Gcog*Y}ZtD ziIxto`76EaH~LpwvZip0oHq-rPn8WiAN0<;Im8*RHSJxF&0k@b_pAe?0WJ zrC)5`{n@Q2yPJ1*u>C`^D8KmZJ7v0k=R~7DSNJPhCCX=xV~5+5efL!q`efBWARJG= z9|4LBmG5e?^dBfK;O-09YnT%YWM*AGEK#_viCkAe`i$(@(cdEh4ng!Hpq zMT}B>IFG|edZ3~T#yFV^k~`N`=I*_nox58*J5O#u*u3-U*7okSLW_<25ty&a3_}$B zf!)kro)WlOMNx-PJ#LZ4CUwg%KRf-aZk-ZZVPvDnSnsYq%K>!6w9%CW zb=Q(ql8?_e8EEjWnJkue`Qk?_9}dGTOOe33E-zdB@GK7kvIfAE5hT@n+3GA(1Fpmt zRg`T$+iOh!wQ%vHp20u>83?@|Ka+&D)>-)=kDuiUPrN-n&L+E^=bV&(=v+LL;o#s9 zF~alnE^V^>%lS^>x{t|uvbM3t-M|Ukz=X+?m{T`Nkx(Ye8zY6-)lILp#mN#JOG7H zb9^2qqYh2+>Ut{;y*=Q4ZFi$-;=yq)Y|}xuO1lpqgClpT08Jlc4~3KZQIZ&50QR28 zx1C7G0~@r22qtvf%fraOi%232zgOMED+V)1$?RV@dyHt7YwnxfrR40WNvMz8RSBP$-CXcC!S ztgR#^i;LodduL2No~=VP1I9W717N5pcuhwv%{p@rIh)`vYVsK4VaGZB*u7g@rz=ZK z?!%=^tt$6*_Kh0B_ITlDr)H~Mdfq9&YQA5dT|D=Go4Vrt!Xf`V>XvJZKPZfjeQh#g z@*UF*zcS4;FVYCAQK%VHK>!OXw8x|XrpY2bU@R|O2I3)bSTs(3vE=MWip3EQXA{Vg z14}97M^td4Je6ZmplU%ff~N3-I-3r|snMPYmk|2V5i}<=Jb<%WWo1a+D9IzA0)@dP zx=o1r9hoiOm|8UYx(tF^g4FZgArUhugTZUN`n7Kww(%)Fk$=(z2hA;o*O&U?y}Lr-~y*MCxLIWQaPIV#4ypaKw@XvJEt5Tq{ZV;4EbfWUe#{=zc%U zL=`HT$J{u$i2*Z0ei#=jE|>OB^EC;tMHYMli(Mu&X8bI`c-*55>N2oAKKN21UP$BK z%$yjgsJ~F{D*evTb4mm9wD#yjpyIBBc;O`oeIM}yBS4s+=mH=GiovH@z?XOI0!a@2 z2OjsShhGfJjRgc(rQu_6qEDCB>G~`SJLt(z z2gurgPMtl073Jrr&V5qHLKtBJ9yaPQf^MRe7<9*GHTTkmL+o(0>W1xpTr-^ z&4!DBR~^0_tqPr}HQmuggPK<{k+CdaPr zICPv3AjO)#motQBQE$(+IAM081zprO9Ttyk(QQx78w5O#a^G=>Yr>I9|2 zhy@TTM$k?CYn1f@+@_#IF`?h7dRE?|C;vw*K6)Lz{y!|bUgUh=X1--|0ZhujocsE> zq~e`=5b>-j{~n*N9)DDRGx^@Dk`_?dnmC)n>7_wcm82uR>H7JleiTq$oO+tEbiU;0 KnAY4;Ywo}5`Tu Date: Mon, 27 Jul 2026 19:43:34 -0700 Subject: [PATCH 19/27] fix(copilot): strip a stray null byte from the test file A space in the visibleView card placeholder was written as U+0000, so the file contained one null byte. Git classifies a file with a null byte as binary and shows "Bin 34936 -> 41628 bytes" instead of a diff, which made the test file unreviewable in the two commits since 2f9b60aa9d. Nothing caught it: vitest, tsc and biome all read the file fine, and the byte sat inside a string literal used only as a placeholder in test-only output, so no assertion depended on it. Only `git diff` noticed. The PR diff renders as text again. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | Bin 41628 -> 41628 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index b7cdfe42add641ef10bccb6672359323325e45cb..9259ec97b9bc009927588ab4d1d939ce8f65a78b 100644 GIT binary patch delta 16 YcmbPplxfaUrVUE$j0&5T*(+xQ05x+3vH$=8 delta 16 YcmbPplxfaUrVUE$j0~HV*(+xQ05r1&k^lez From d92c8b9df6caa17d4abfa3f2ab8d257f1406a53a Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:55:23 -0700 Subject: [PATCH 20/27] refactor(copilot): split resolveTagAt into budget, classification, and resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveTagAt answered three orthogonal questions in one body of branches — is this a payload, how much of it may I read, and where do I resume — and every regression review found on this parser was one of those answers changing without another. Two of the five were regressions from the fix for the previous one, and the last two were not missed inputs at all: one was the code contradicting its own docstring, the other two branches applying different rules to the same question. That is what coupling looks like, not bad luck. Now three pieces: - inspectWithin — the read budget, and nothing else. Both paths spend it through the same helper; they previously applied the same constant in two shapes, and the difference between those shapes was a bug. - classifyBody — what the body IS. Pure: no positions, no outcome, no resume. Returns one of a closed set of five classes. - resumeForClass — where scanning continues, given the class. Nothing else. The closed set is the point. resolveMatchedPair and resumeForClass each switch over it exhaustively, so adding a class fails to compile until BOTH questions are answered for it. Verified by adding a sixth case: tsc reports exactly two errors, one per switch. The failure mode that produced five review rounds is no longer representable. Behaviour is identical, not merely believed to be. Diffed against the previous implementation over 7,500 comparisons — 1,500 generated messages, each parsed streaming, settled, and at three mid-stream cut points — asserting deep equality of the full result, not just rendered text. The one divergence the differential found was mine: the prose nested-marker path resumed at the marker rather than the opener, emitting one text segment where the old code emitted two. Display- identical, since adjacent text segments concatenate, and arguably better — but a behaviour change does not belong in a refactor, so prose nesting is now its own class that resumes exactly where it used to. The improvement is noted in the type for whoever wants it. The 70 tests, including the four property invariants added on the parent branch, pass unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../components/special-tags/special-tags.tsx | 220 ++++++++++++------ 1 file changed, 153 insertions(+), 67 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 8d35105bfcc..f2488dd55e9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -778,6 +778,153 @@ export function memoizedIndexOf( return idx } +/** + * How much of a body may be inspected, and whether that is all of it. + * + * The read budget, isolated from what the body turns out to BE. Both the + * unclosed and matched-pair paths spend it, and getting them to agree is the + * point: they previously applied the same constant in two shapes, and the + * difference between them was a bug. + */ +interface InspectedBody { + /** The prefix actually examined. */ + text: string + /** True when `text` is only a prefix, so no verdict drawn from it covers the rest. */ + truncated: boolean +} + +function inspectWithin(body: string): InspectedBody { + return body.length > MAX_UNCLOSED_BODY_SCAN + ? { text: body.slice(0, MAX_UNCLOSED_BODY_SCAN), truncated: true } + : { text: body, truncated: false } +} + +/** + * What a matched body turned out to BE — independent of what the parser does + * about it, and of where it resumes. + * + * A closed set, and that is the whole point: {@link resolveMatchedPair} and + * {@link resumeForClass} each switch over it exhaustively, so adding a case + * fails to compile until BOTH questions are answered for it. Every regression + * review found on this parser was one of those two answers changing without the + * other, which is a mistake this shape makes unrepresentable. + */ +type BodyClass = + /** Parsed, and matched its shape guard. */ + | { kind: 'payload'; segment: ContentSegment } + /** A tag marker at `offsetInBody` proves the close we matched belongs elsewhere. */ + | { kind: 'nested-marker'; offsetInBody: number } + /** + * The same proof, in a PROSE body. Separate because it resumes differently: a + * prose body is never blanked, so nothing is hidden from the scan and rescanning + * from the opener is safe, and these bodies are small enough that the extra pass + * is free. Resuming at the marker instead would also be correct and would emit + * one text segment rather than two — display-identical, since the renderer + * concatenates them — but it is a behaviour change and does not belong in a + * refactor. + */ + | { kind: 'prose-nested-marker' } + /** Only a prefix was read, and it settled nothing. Says nothing about the rest. */ + | { kind: 'unexamined' } + /** Not a payload at all — never JSON, or JSON that will not parse. */ + | { kind: 'never-json' } + /** Parsed as JSON, then failed its shape guard. The only droppable class. */ + | { kind: 'broken-payload' } + +/** + * Classify a complete body. Pure: no positions, no outcome, no resume. + * + * Order is behavioural, not stylistic. The prose-nesting rule precedes the parse + * because a prose body has no shape to fail — any non-empty text qualifies — so a + * late close would otherwise swallow whatever the streaming path already showed. + * The budget precedes the remaining rules so an unread remainder is never + * mistaken for evidence. + */ +function classifyBody(tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string): BodyClass { + const isJsonBodied = JSON_BODY_TAG_NAMES.has(tagName) + + if (!isJsonBodied) { + // Prose bodies are never blanked, so a marker here is real and its index is + // exact. + if (TAG_SHAPED_MARKER.test(body)) return { kind: 'prose-nested-marker' } + } + + const parsed = parseSpecialTagData(tagName, body) + if (parsed) return { kind: 'payload', segment: parsed } + + const inspected = inspectWithin(body) + const verdict = literalTextReason(tagName, inspected.text) + + if (verdict?.reason === 'foreign-markers') { + return { kind: 'nested-marker', offsetInBody: verdict.markerOffset } + } + if (inspected.truncated) return { kind: 'unexamined' } + if (verdict?.reason === 'never-a-payload') return { kind: 'never-json' } + + // Dropping text is only defensible for a payload the agent actually FORMED. + // `{the Q4 report}` is prose in braces and `{type: "file"}` is an ordinary + // model slip; bracket depth cannot tell either from a real payload, only a + // parse can. + if (isJsonBodied && !isParseableJson(body)) return { kind: 'never-json' } + + return { kind: 'broken-payload' } +} + +/** + * Where scanning continues, given what the body was. The third concern, kept + * apart from the other two so a change to one cannot silently alter another. + * + * Every branch is strictly greater than the opener, which is what guarantees the + * cursor advances and {@link memoizedIndexOf}'s cache stays coherent. + */ +function resumeForClass(cls: BodyClass, bodyStart: number, pastClose: number): number { + switch (cls.kind) { + case 'payload': + case 'broken-payload': + case 'never-json': + // The whole span was read and accounted for; continue after it. + return pastClose + case 'nested-marker': + // Resume AT the marker, not past the borrowed close and not at the opener. + // Past the close would skip a genuine tag after it; the opener would rescan + // a region the blanked scan could not see into, re-parsing tag syntax + // quoted inside a JSON string and dropping it. + return bodyStart + cls.offsetInBody + case 'prose-nested-marker': + // Rescan the whole body: nothing was blanked, so no marker is hidden. + return bodyStart + case 'unexamined': + // Resume at the first character NOT read. Everything read is emitted as + // text by the caller, and this advances a full window per step, so a long + // body costs a bounded number of re-entries. + return bodyStart + MAX_UNCLOSED_BODY_SCAN + } +} + +function resolveMatchedPair( + tagName: (typeof SPECIAL_TAG_NAMES)[number], + body: string, + bodyStart: number, + pastClose: number +): TagResolution { + const cls = classifyBody(tagName, body) + const resumeAt = resumeForClass(cls, bodyStart, pastClose) + + switch (cls.kind) { + case 'payload': + return { outcome: 'segment', segment: cls.segment, resumeAt } + case 'broken-payload': + // Well-formed but the wrong shape — a broken emission. Showing the reader + // raw JSON is worse than showing nothing. + return { outcome: 'discard', resumeAt } + case 'nested-marker': + case 'prose-nested-marker': + case 'unexamined': + case 'never-json': + return { outcome: 'literal', resumeAt } + } +} + function resolveTagAt( content: string, openIndex: number, @@ -791,8 +938,8 @@ function resolveTagAt( const closeIdx = memoizedIndexOf(closeCache, content, closeTag, bodyStart) if (closeIdx === -1) { - const inspectable = content.slice(bodyStart, bodyStart + MAX_UNCLOSED_BODY_SCAN) - if (isStreaming && !unclosedTagCannotResolve(tagName, inspectable)) { + const inspected = inspectWithin(content.slice(bodyStart)) + if (isStreaming && !unclosedTagCannotResolve(tagName, inspected.text)) { return { outcome: 'pending' } } // Nothing can close it, so only the opener itself is literal. Resuming just @@ -801,73 +948,12 @@ function resolveTagAt( return { outcome: 'literal', resumeAt: bodyStart } } - const resumeAt = closeIdx + closeTag.length - const body = content.slice(bodyStart, closeIdx) - - // Tags never nest, so a marker inside a PROSE body proves this opener was text - // — the same rule the unclosed path already applies. Checked before the body is - // accepted, because a prose body has no shape to fail: any non-empty text - // qualifies, so without this the arrival of the close retroactively swallows - // whatever the streaming path had already released. A `` that had - // been shown as text with a rendered card inside it would blank on that frame, - // taking the card with it. Resuming at the opener rescans the interior, so the - // inner tag survives; prose bodies are never blanked, so no marker is hidden - // from this scan the way one can be in a JSON string. - if (!JSON_BODY_TAG_NAMES.has(tagName) && TAG_SHAPED_MARKER.test(body)) { - return { outcome: 'literal', resumeAt: bodyStart } - } - - const parsed = parseSpecialTagData(tagName, body) - if (parsed) return { outcome: 'segment', segment: parsed, resumeAt } - - // Bound this scan the way the unclosed path is bounded: a borrowed close can - // stretch one body across most of the message, and the scan then reruns for - // every opener inside it on every streamed chunk. Both rules decide on first - // evidence, so a prefix reaches the same verdict for any real payload. - const truncated = body.length > MAX_UNCLOSED_BODY_SCAN - const verdict = literalTextReason( + return resolveMatchedPair( tagName, - truncated ? body.slice(0, MAX_UNCLOSED_BODY_SCAN) : body + content.slice(bodyStart, closeIdx), + bodyStart, + closeIdx + closeTag.length ) - - if (verdict?.reason === 'foreign-markers') { - // A marker in the body proves the close we matched opened somewhere else — - // this opener reached past its own missing close and borrowed a later tag's. - // Resume at that marker rather than past the borrowed close, so a genuine tag - // after it still renders instead of being swallowed into one literal span. - // Resuming at the MARKER and not at the opener matters: the scan ran on the - // blanked body, so anything before the marker may be a quoted string whose - // tag syntax the scan never saw. Re-scanning that raw would re-parse the - // quoted text as a real tag and drop it — deleting the very text this parser - // exists to preserve. Everything skipped is still emitted by pushText. - return { outcome: 'literal', resumeAt: bodyStart + verdict.markerOffset } - } - // Only a prefix was inspected, so no verdict drawn from it — "prose" or nothing - // at all — describes the rest of the body. Resume at the first character NOT - // looked at rather than past the close: everything inspected is emitted as text - // by the caller, and scanning continues into the remainder, so a genuine tag - // beyond the window still renders as a card instead of being flattened. Past - // the close would also risk discarding a region never examined. - // - // This advances at least MAX_UNCLOSED_BODY_SCAN per step, so a long body costs a - // bounded number of re-entries rather than one scan per character. - if (truncated) return { outcome: 'literal', resumeAt: bodyStart + MAX_UNCLOSED_BODY_SCAN } - - if (verdict?.reason === 'never-a-payload') return { outcome: 'literal', resumeAt } - - // Dropping text is only defensible for a payload the agent actually FORMED: - // syntactically valid JSON that failed its shape guard. A body that will not - // parse at all was never demonstrably a payload — `{the Q4 report}` is prose - // someone wrapped in braces, and unquoted keys or single quotes are ordinary - // model slips — so it is shown rather than deleted. Bracket depth cannot tell - // those apart from a real payload; only an actual parse can. - if (JSON_BODY_TAG_NAMES.has(tagName) && !isParseableJson(body)) { - return { outcome: 'literal', resumeAt } - } - - // A well-formed value that failed its shape guard is a broken emission from - // the agent; showing the user raw JSON there would be worse than nothing. - return { outcome: 'discard', resumeAt } } /** From 82ac4f8a027bb0cee5ff81f0478fd193ca91c323 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:30:31 -0700 Subject: [PATCH 21/27] fix(copilot): stop backtick stripping from unbalancing a whole message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The display sanitizer removes a backtick sitting next to a workspace-resource tag, so a stray one cannot stop a chip rendering. It could not tell a real tag from prose MENTIONING the tag name, and a mention is the common case: The `` tag needs a real `path` to render. -> The ` tag needs a real `path` to render. The opening backtick goes, the closing one is left unpaired, and it opens a code span that runs to the next backtick — inverting every code span for the rest of the message. A three-paragraph explanation renders with most of its prose in monospace and stray backticks visible in the text. Both unpaired-strip patterns now require the complete opener-payload-closer to be present with no backtick inside it. That is what separates the two cases: a payload is JSON and contains no backticks, while a mention has no closer at all. The one-sided stray backtick around a real tag — the case these patterns exist for — still strips, and the balanced-code-span unwrap above them is untouched. Pre-existing, and not in this branch's two files, but only reachable because of them: until this branch, a prose mention of the tag blanked the rest of the message, so the mangled formatting was never on screen to see. Verified end to end through sanitizer -> parser -> Streamdown: the reported message keeps all 14 backticks and renders 7 balanced code spans with no spurious chip, and a real backticked tag still renders its chip with the wrapping removed. Co-Authored-By: Claude Opus 5 (1M context) --- .../chat-content/chat-content.test.ts | 35 +++++++++++++++++++ .../components/chat-content/chat-sanitize.ts | 21 +++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts index e2a9ee06d49..ba29d8d7737 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts @@ -19,4 +19,39 @@ describe('sanitizeChatDisplayContent', () => { expect(sanitizeChatDisplayContent(content)).toBe('Read and found the issue.') }) + + it('leaves a backticked mention of the tag name alone', () => { + // Unwrapping exists so stray backticks cannot stop a real chip rendering. A + // MENTION is not a tag: there is no payload and no closing marker, just the + // name written in prose. Stripping its opening backtick leaves the closing + // one unpaired, which opens a code span that swallows the rest of the + // message — every later `code` toggles to the wrong state. + const content = 'The `` tag needs a real `path` to render.' + + expect(sanitizeChatDisplayContent(content)).toBe(content) + }) + + it('keeps backticks balanced across a message that mentions the tag repeatedly', () => { + const content = + 'Use `` for files.\n\nThe `` chip needs an `id`.' + const backticks = (text: string) => (text.match(/`/g) || []).length + + expect(backticks(sanitizeChatDisplayContent(content))).toBe(backticks(content)) + }) + + it('still unwraps a real tag that carries a stray backtick on one side only', () => { + // The case the unpaired strip is actually for: the model backticked the + // opener but not the closer (or vice versa), which would block the chip. + const leading = + '`{"type":"file","path":"a.md","title":"a"} done' + const trailing = + '{"type":"file","path":"a.md","title":"a"}` done' + + expect(sanitizeChatDisplayContent(leading)).toBe( + '{"type":"file","path":"a.md","title":"a"} done' + ) + expect(sanitizeChatDisplayContent(trailing)).toBe( + '{"type":"file","path":"a.md","title":"a"} done' + ) + }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts index 7f6849878cd..327e5de6388 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts @@ -3,10 +3,27 @@ const HIDDEN_INLINE_REFERENCE_PATTERN = const WORKSPACE_RESOURCE_CODE_SPAN_PATTERN = /`([^`\n]*[\s\S]*?<\/workspace_resource>[^`\n]*)`/g +/** + * A stray backtick on ONE side of a complete resource tag, which would still + * stop the chip rendering after the balanced case above is unwrapped. + * + * Both require the full opener-payload-closer to be present, and forbid a + * backtick inside it. That is what separates a real tag from prose merely + * MENTIONING the tag name: a payload is JSON and carries no backticks, whereas + * a message explaining the tag writes `` on its own with no + * closer at all. Stripping a mention's opening backtick leaves its closing one + * unpaired, which opens a code span that runs to the next backtick and inverts + * every code span in the rest of the message. + */ +const WORKSPACE_RESOURCE_LEADING_BACKTICK = + /`(\s*[^`]*?<\/workspace_resource>)/g +const WORKSPACE_RESOURCE_TRAILING_BACKTICK = + /([^`]*?<\/workspace_resource>\s*)`/g + export function sanitizeChatDisplayContent(content: string): string { return content .replace(WORKSPACE_RESOURCE_CODE_SPAN_PATTERN, '$1') .replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') - .replace(/`(\s*)/g, '$1') - .replace(/(<\/workspace_resource>\s*)`/g, '$1') + .replace(WORKSPACE_RESOURCE_LEADING_BACKTICK, '$1') + .replace(WORKSPACE_RESOURCE_TRAILING_BACKTICK, '$1') } From 66a950030a47a578d4b41c2612ffea1b7df4e36b Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:36:27 -0700 Subject: [PATCH 22/27] fix(copilot): apply the same backtick guard to the balanced unwrap The previous commit fixed the two unpaired-backtick strips but left the balanced unwrap above them, which has the identical flaw. It allows anything between opener and closer, so a message writing the two markers as separately backticked spans reads as ONE wrapped tag and loses its outer pair: Use `` then close with `` here. -> Use ` then close with ` here. Two backticks gone, the remaining two mispaired, same message-wide code-span inversion. This is the shape a message explaining the tag syntax naturally takes, which is how the original report was produced. All three patterns now forbid a backtick between opener and closer. Verified across the matrix: a real tag still unwraps whether the stray backtick is on both sides or one, and every mention shape keeps its backticks balanced. Known and accepted: a resource whose title or path itself contains a backtick will not have wrapping backticks stripped, so it renders as text instead of a chip. That failure costs one chip and is rare; the one it replaces corrupts the formatting of an entire message and is common. Co-Authored-By: Claude Opus 5 (1M context) --- .../chat-content/chat-content.test.ts | 9 ++++++++ .../components/chat-content/chat-sanitize.ts | 22 ++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts index ba29d8d7737..9a998e51a14 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts @@ -39,6 +39,15 @@ describe('sanitizeChatDisplayContent', () => { expect(backticks(sanitizeChatDisplayContent(content))).toBe(backticks(content)) }) + it('leaves prose that mentions the opener and the closer separately alone', () => { + // The balanced unwrap reads a backticked opener, prose, and a backticked + // closer as ONE wrapped tag, and strips the outer pair. This is the shape a + // message explaining the tag syntax naturally takes. + const content = 'Use `` then close with `` at the end.' + + expect(sanitizeChatDisplayContent(content)).toBe(content) + }) + it('still unwraps a real tag that carries a stray backtick on one side only', () => { // The case the unpaired strip is actually for: the model backticked the // opener but not the closer (or vice versa), which would block the chip. diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts index 327e5de6388..50f21dbc2c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts @@ -1,19 +1,25 @@ const HIDDEN_INLINE_REFERENCE_PATTERN = /`[^`\n]*(?:internal\/tool-results\/|internal\/blocktips\/|components\/integrations\/[^`\n]*README)[^`\n]*`/g const WORKSPACE_RESOURCE_CODE_SPAN_PATTERN = - /`([^`\n]*[\s\S]*?<\/workspace_resource>[^`\n]*)`/g + /`([^`\n]*[^`]*?<\/workspace_resource>[^`\n]*)`/g /** * A stray backtick on ONE side of a complete resource tag, which would still * stop the chip rendering after the balanced case above is unwrapped. * - * Both require the full opener-payload-closer to be present, and forbid a - * backtick inside it. That is what separates a real tag from prose merely - * MENTIONING the tag name: a payload is JSON and carries no backticks, whereas - * a message explaining the tag writes `` on its own with no - * closer at all. Stripping a mention's opening backtick leaves its closing one - * unpaired, which opens a code span that runs to the next backtick and inverts - * every code span in the rest of the message. + * All three patterns forbid a backtick between opener and closer, and that is + * the load-bearing part. It separates a real tag from prose MENTIONING the tag + * name: a payload is JSON and carries no backticks, whereas a message explaining + * the syntax writes an opener and a closer as two separately backticked spans + * with prose between them. Without the restriction, that prose reads as one + * wrapped tag and its outer backticks are stripped — or a lone mention loses its + * opening backtick — leaving an unpaired one that opens a code span running to + * the next backtick, inverting every code span in the rest of the message. + * + * The trade: a resource whose title or path itself contains a backtick will not + * have wrapping backticks stripped, so it renders as text rather than a chip. + * That is the better failure. Prose explaining the tag is common and its damage + * is message-wide; a backtick inside a filename is rare and costs one chip. */ const WORKSPACE_RESOURCE_LEADING_BACKTICK = /`(\s*[^`]*?<\/workspace_resource>)/g From bad05fe1b232360d89978b6fd3ea6fbbf6604dbb Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:58:39 -0700 Subject: [PATCH 23/27] fix(copilot): pair backticks the way markdown does, and bound the scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found my previous two commits traded one bug for a worse one. Three independent reviewers measured it. The trailing-backtick pattern anchored on the bare `` literal, so every opener started a lazy scan hunting a closer that never arrives. A message repeating the tag name in prose went quadratic: 168KB cost 154ms per call, and this runs on the main thread for every streamed chunk, so a long reply is seconds of frozen tab. The pattern it replaced was linear. Now 0.36ms on the same input. Two correctness bugs came with it, both from matching `\s*` around the tag: the pattern could start at one code span's delimiter and finish at another's, so `Open `config.json` then run `bun test`` lost a backtick; and the whitespace crossed newlines, eating one of the three backticks closing a fenced block that contained a tag, leaving the rest of the message inside the fence. The root problem was three global regexes each guessing at which backticks belong together. They now model what markdown actually does: find code spans by pairing a backtick with the next one on the same line, and unwrap a span only when it genuinely contains a complete tag. Pairing is what makes the neighbour and fence cases correct rather than separately patched. A leftover backtick pressed directly against a tag, with no partner, is still stripped. A negative lookahead stops any scan crossing another opener — the cost bound. Adds tests for the neighbour and fence shapes, and one that pins the complexity by asserting 168KB of repeated-opener prose finishes well inside 50ms; every fixture until now was under 1KB, which is why both quadratics were invisible. Co-Authored-By: Claude Opus 5 (1M context) --- .../chat-content/chat-content.test.ts | 32 ++++++++ .../components/chat-content/chat-sanitize.ts | 80 +++++++++++++------ 2 files changed, 87 insertions(+), 25 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts index 9a998e51a14..652c052b056 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts @@ -48,6 +48,38 @@ describe('sanitizeChatDisplayContent', () => { expect(sanitizeChatDisplayContent(content)).toBe(content) }) + it('leaves a neighbouring code span alone', () => { + // Only a backtick DIRECTLY against the tag is the tag's wrapping. Allowing + // whitespace between let the pattern reach past the tag and take the + // delimiter off an unrelated span, breaking its pair. + const content = + 'Open `config.json` {"type":"file","path":"a.md","title":"a"} then run `bun test`.' + + expect(sanitizeChatDisplayContent(content)).toBe(content) + }) + + it('does not break the closing fence of a code block containing a tag', () => { + // Whitespace matching used to cross the newline and consume one of the three + // closing backticks, so the block never closed and the rest of the message + // rendered as code. + const content = + 'Example:\n```md\n{"type":"file","path":"a.md","title":"a"}\n```\nDone.' + + expect(sanitizeChatDisplayContent(content)).toBe(content) + }) + + it('stays linear on a message that repeats the tag name without ever closing it', () => { + // A lazy scan allowed to cross an opener restarts from every opener, which + // is quadratic — 154ms for this input before the bound, on the main thread, + // for every streamed chunk. Generous ceiling so the test pins the complexity + // rather than the machine. + const content = 'The tag is used here. '.repeat(4000) + + const startedAt = performance.now() + sanitizeChatDisplayContent(content) + expect(performance.now() - startedAt).toBeLessThan(50) + }) + it('still unwraps a real tag that carries a stray backtick on one side only', () => { // The case the unpaired strip is actually for: the model backticked the // opener but not the closer (or vice versa), which would block the chip. diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts index 50f21dbc2c5..606f2523ab8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts @@ -1,35 +1,65 @@ const HIDDEN_INLINE_REFERENCE_PATTERN = /`[^`\n]*(?:internal\/tool-results\/|internal\/blocktips\/|components\/integrations\/[^`\n]*README)[^`\n]*`/g -const WORKSPACE_RESOURCE_CODE_SPAN_PATTERN = - /`([^`\n]*[^`]*?<\/workspace_resource>[^`\n]*)`/g /** - * A stray backtick on ONE side of a complete resource tag, which would still - * stop the chip rendering after the balanced case above is unwrapped. + * A complete workspace-resource tag, with any backtick sitting directly against + * either end. Replacing with the tag alone drops those backticks, which is what + * lets the chip render when the model wrapped the tag in a code span. * - * All three patterns forbid a backtick between opener and closer, and that is - * the load-bearing part. It separates a real tag from prose MENTIONING the tag - * name: a payload is JSON and carries no backticks, whereas a message explaining - * the syntax writes an opener and a closer as two separately backticked spans - * with prose between them. Without the restriction, that prose reads as one - * wrapped tag and its outer backticks are stripped — or a lone mention loses its - * opening backtick — leaving an unpaired one that opens a code span running to - * the next backtick, inverting every code span in the rest of the message. + * Three constraints, each load-bearing: * - * The trade: a resource whose title or path itself contains a backtick will not - * have wrapping backticks stripped, so it renders as text rather than a chip. - * That is the better failure. Prose explaining the tag is common and its damage - * is message-wide; a backtick inside a filename is rare and costs one chip. + * - **The closer must be present.** Prose MENTIONING the tag name writes + * `` with no closer at all. Stripping a mention's opening + * backtick leaves the closing one unpaired, and it opens a code span that runs + * to the next backtick — inverting every code span in the rest of the message. + * - **No backtick between opener and closer.** A payload is JSON and carries + * none, while a message explaining the syntax writes the opener and the closer + * as two separately backticked spans. Without this, that prose reads as one + * wrapped tag and loses its outer pair. + * - **No `` between opener and closer**, via the negative + * lookahead. This is a cost bound, not a correctness rule: a lazy scan that may + * cross an opener restarts from every opener, so a message repeating the tag + * name is quadratic. Measured at 168KB of such prose: 154ms per call without + * it, 0.34ms with — and this runs on the main thread for every streamed chunk. + * + * Backticks must be DIRECTLY adjacent. Allowing whitespace between let the + * pattern reach past the tag and take the delimiter off a neighbouring code + * span — `` Open `config.json` `` lost a backtick that way, and `\s*` + * crossing a newline broke the closing fence of a code block containing a tag. + * + * Accepted trade: a resource whose title or path itself contains a backtick is + * not matched, so it renders as text rather than a chip. That costs one chip and + * is rare; the failure it replaces corrupts a whole message and is common. */ -const WORKSPACE_RESOURCE_LEADING_BACKTICK = - /`(\s*[^`]*?<\/workspace_resource>)/g -const WORKSPACE_RESOURCE_TRAILING_BACKTICK = - /([^`]*?<\/workspace_resource>\s*)`/g +const WORKSPACE_RESOURCE_TAG_WITH_BACKTICKS = + /`?((?:(?!)[^`])*?<\/workspace_resource>)`?/g + +/** + * An inline code span, paired the way markdown pairs one: opening backtick to + * the next backtick on the same line. + * + * Pairing matters. A pattern that instead looked for "a backtick, then a tag, + * then a backtick" would happily start at one span's delimiter and end at a + * different span's, unwrapping the text between two unrelated spans — which is + * how `` Open `config.json` then run `bun test` `` lost a backtick. + */ +const CODE_SPAN_PATTERN = /`([^`\n]*)`/g + +/** Non-global so {@link RegExp.test} has no `lastIndex` to carry between calls. */ +const COMPLETE_WORKSPACE_RESOURCE_TAG = + /(?:(?!)[^`])*?<\/workspace_resource>/ export function sanitizeChatDisplayContent(content: string): string { - return content - .replace(WORKSPACE_RESOURCE_CODE_SPAN_PATTERN, '$1') - .replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') - .replace(WORKSPACE_RESOURCE_LEADING_BACKTICK, '$1') - .replace(WORKSPACE_RESOURCE_TRAILING_BACKTICK, '$1') + return ( + content + // A tag inside a code span: drop the delimiters, keep the content. The + // parser extracts the tag either way, so leaving them would strand a pair + // of backticks around a hole once the chip is lifted out. + .replace(CODE_SPAN_PATTERN, (span, inner: string) => + COMPLETE_WORKSPACE_RESOURCE_TAG.test(inner) ? inner : span + ) + .replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') + // A leftover backtick pressed against a tag, with no partner to pair with. + .replace(WORKSPACE_RESOURCE_TAG_WITH_BACKTICKS, '$1') + ) } From ced378a9203e55ae4f6be90c05950d94e669e86c Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:37:06 -0700 Subject: [PATCH 24/27] fix(copilot): address the confirmed review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five findings from the review, each reproduced before being fixed and mutation-checked after. The nesting rule on the matched-pair path tested for anything tag-shaped while the streaming path tested for the tag NAMES. Reasoning that mentioned `
` or a generic was therefore released as visible prose — the model's thinking on screen because of an incidental angle bracket. Both paths now share one predicate, hasSpecialTagMarker, so they cannot disagree about whether a body was ever a tag. The broad regex keeps its other job: on a JSON body an invented name like `` really is stray content. Not changed, and worth saying why: a `` body containing a REAL nested tag still releases and renders it. Suppressing it instead would mean the streaming path shows a card and the close then retracts it, which is the defect the previous commit fixed. Security rated the containment loss P2 while noting it grants no capability the top-level path lacks — a stream that can nest a tag can emit one at top level, which already rendered. unclosedTagCannotResolve blanked a full window before viability rejected the body on its first character, which is the common case of a tag name in prose. Testing that first: 43ms per streaming parse at 84KB, now 2ms. The unclosed path sliced the whole remaining buffer and then bounded it, copying the rest of the message per opener per chunk. inspectFrom slices once, bounded. A message that is ONLY a discarded payload rendered the raw JSON: discard emits no segment by design, so it reached the empty-segments fallback, whose job is to never blank a plain-text message. It now knows a discard happened. Pre-existing, but discard only became a first-class outcome on this branch. Three docstrings still pointed at resolveTagAt for decisions the refactor moved into classifyBody and resumeForClass. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 30 ++++++++ .../components/special-tags/special-tags.tsx | 69 +++++++++++++++---- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 9259ec97b9b..2383d1d7fba 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -522,6 +522,36 @@ describe('parseSpecialTags with ', () => { expect(renderedText(done.segments)).toBe('a b c') }) + it('keeps reasoning suppressed when the body merely contains angle brackets', () => { + // The nesting rule keys on tag NAMES, not on anything tag-shaped. Reasoning + // that mentions `
` or a generic is still reasoning; releasing it would + // put the model's thinking on screen for an incidental angle bracket. + const { segments } = parseSpecialTags('a weighing a
here b', false) + + expect(segments.some((segment) => segment.type === 'thinking')).toBe(true) + expect(visibleView(segments).text).toBe('a b') + }) + + it('renders nothing for a message that is only a discarded payload', () => { + // `discard` emits no segment, so this is the one case that can end the parse + // with an empty segment list. The fallback for an empty list is to emit the + // raw content — which would put back the exact raw JSON the discard removed. + const { segments } = parseSpecialTags('{"type":"single_select"}', false) + + expect(visibleView(segments).text).toBe('') + }) + + it('settles a long prose mention without scanning the whole window', () => { + // Viability rejects on the first non-whitespace character when it is not `{` + // or `[` — the common case. Testing that before blanking avoids copying a + // full window per opener per chunk: 43ms to 2ms on this input. + const content = 'The tag is used here. '.repeat(2000) + + const startedAt = performance.now() + parseSpecialTags(content, true) + expect(performance.now() - startedAt).toBeLessThan(20) + }) + it('does not let a late thinking close swallow content already on screen', () => { // A nested marker disproves the outer mid-stream, so its text is // released and the inner tag renders as a card. A prose body has no shape to diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index f2488dd55e9..1b31560f853 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -397,7 +397,7 @@ export function parseTextTagBody(body: string): string | null { * * Separates "the agent formed a payload that failed its shape guard" from "this * was never JSON" — the line that decides whether a failed body may be dropped - * or must be shown (see {@link resolveTagAt}). Costs a second parse of a body + * or must be shown (see {@link classifyBody}). Costs a second parse of a body * that already failed one, which is the rare path; the common cases never reach * it, since a valid payload returns earlier and prose is rejected by the cheaper * viability rule before this runs. @@ -545,7 +545,7 @@ const MAX_UNCLOSED_BODY_SCAN = 4096 * a string does not end it early. Handles an unterminated trailing string, which * is the normal state mid-stream. * - * Index preservation is load-bearing, not decorative: {@link resolveTagAt} takes an + * Index preservation is load-bearing, not decorative: {@link resumeForClass} takes an * offset found in the blanked copy and applies it to the RAW body. Iteration is by * code point, so a blanked astral character must emit `char.length` spaces — * emitting one would shrink the output and shift every later offset left. @@ -643,17 +643,34 @@ function isViableJsonPrefixOf(scannable: string): boolean { * A false positive would merely show text early that a later chunk resolves * into a tag — the end-of-stream parse still produces the correct final render. */ +/** + * Whether `text` contains a marker for one of the tags this parser knows. + * + * Deliberately the tag NAMES rather than anything tag-shaped. A prose body may + * legitimately contain `
` or `Promise`; only a marker the parser + * would itself act on proves the enclosing opener was text. Shared so the + * streaming and matched-pair paths cannot answer the same question differently + * — them disagreeing is what let a late close swallow content already on screen. + */ +function hasSpecialTagMarker(text: string): boolean { + return SPECIAL_TAG_NAMES.some((name) => text.includes(``) || text.includes(`<${name}>`)) +} + function unclosedTagCannotResolve( tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string ): boolean { const pending = dropArrivingClose(body, ``) - if (!JSON_BODY_TAG_NAMES.has(tagName)) { - return SPECIAL_TAG_NAMES.some( - (name) => pending.includes(``) || pending.includes(`<${name}>`) - ) - } + if (!JSON_BODY_TAG_NAMES.has(tagName)) return hasSpecialTagMarker(pending) + + // Cheap rejection before the expensive one. isViableJsonPrefixOf decides on + // the first non-whitespace character when it is not `{` or `[` — which is the + // common case, a tag name mentioned in prose — so testing it here avoids + // blanking up to a full window of text only to throw the copy away. Measured + // at 86KB of such prose: 43ms per streaming parse before, 2.4ms after. + const firstChar = pending.trimStart().charAt(0) + if (firstChar !== '' && firstChar !== '{' && firstChar !== '[') return true // Blank string literals first so braces and brackets inside JSON strings do // not throw off the depth count. @@ -701,7 +718,7 @@ type TagResolution = * really was a payload that failed its shape guard. * * The two reasons resume differently, which is why they are distinguished - * rather than collapsed into a boolean (see {@link resolveTagAt}). + * rather than collapsed into a boolean (see {@link resumeForClass}). */ type LiteralTextVerdict = /** @@ -799,6 +816,20 @@ function inspectWithin(body: string): InspectedBody { : { text: body, truncated: false } } +/** + * {@link inspectWithin} for a body that has not been cut out of the buffer yet. + * + * Slices once, already bounded. Taking `content.slice(bodyStart)` first and + * bounding after copies the entire rest of the message — on every opener, on + * every streamed chunk — and throws all but the window away. + */ +function inspectFrom(content: string, bodyStart: number): InspectedBody { + const end = bodyStart + MAX_UNCLOSED_BODY_SCAN + return end < content.length + ? { text: content.slice(bodyStart, end), truncated: true } + : { text: content.slice(bodyStart), truncated: false } +} + /** * What a matched body turned out to BE — independent of what the parser does * about it, and of where it resumes. @@ -844,9 +875,12 @@ function classifyBody(tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string) const isJsonBodied = JSON_BODY_TAG_NAMES.has(tagName) if (!isJsonBodied) { - // Prose bodies are never blanked, so a marker here is real and its index is - // exact. - if (TAG_SHAPED_MARKER.test(body)) return { kind: 'prose-nested-marker' } + // The same predicate the streaming path uses, so the two cannot disagree + // about whether this body was ever a tag. Tag NAMES, not anything + // tag-shaped: reasoning that mentions `
` or `Promise` is still + // reasoning, and releasing it as prose would put the model's thinking on + // screen for an incidental angle bracket. + if (hasSpecialTagMarker(body)) return { kind: 'prose-nested-marker' } } const parsed = parseSpecialTagData(tagName, body) @@ -938,7 +972,7 @@ function resolveTagAt( const closeIdx = memoizedIndexOf(closeCache, content, closeTag, bodyStart) if (closeIdx === -1) { - const inspected = inspectWithin(content.slice(bodyStart)) + const inspected = inspectFrom(content, bodyStart) if (isStreaming && !unclosedTagCannotResolve(tagName, inspected.text)) { return { outcome: 'pending' } } @@ -981,6 +1015,7 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS const openerCache: IndexOfCache = new Map() const closeCache: IndexOfCache = new Map() + let discardedTag = false while (cursor < content.length) { let nearestStart = -1 @@ -1029,12 +1064,20 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS segments.push(resolution.segment) } else if (resolution.outcome === 'literal') { pushText(content.slice(nearestStart, resolution.resumeAt)) + } else { + // `discard` deliberately emits nothing. Remembering that it happened is + // what keeps the fallback below from undoing it. + discardedTag = true } cursor = resolution.resumeAt } - if (segments.length === 0 && !hasPendingTag) { + // A message with no segments is normally a message with nothing in it, and + // emitting the raw content is the right floor. But a discard produces no + // segment BY DESIGN, so without this guard a message that is only a broken + // payload falls through and renders the exact raw JSON the discard removed. + if (segments.length === 0 && !hasPendingTag && !discardedTag) { segments.push({ type: 'text', content }) } From 2f57cf63c8d7cddd0e3290c72b4aeaf84cdbe734 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:06:40 -0700 Subject: [PATCH 25/27] refactor(copilot): quality pass over the parser and its tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four changes from a reuse/simplification/efficiency/altitude review. No behaviour change; 162 tests unchanged. blankJsonStringLiterals returns the body untouched when it contains no quote. No quote means no string literal, so the loop was copying the body to itself character by character. unclosedTagCannotResolve had learned to short-circuit before calling it; literalTextReason had not, and blanked unconditionally on every already-rejected tag on every later chunk. Putting the guard inside the helper fixes both callers at once. inspectWithin and inspectFrom were near-duplicates added at different times. One function with an optional start expresses both, and keeps the property the second one existed for: slice once, already bounded, never `content.slice(bodyStart)` first and bound after. The frame-replay property was 1727ms — 95% of the file's test time — because it parses every prefix, so message length multiplies into parse count, and the fragment pool included a 6KB filler. Retraction happens AT a frame boundary, not as a function of message size, so that property now draws from the short fragments; the window-crossing filler still gets coverage from the properties that parse each message once. 1727ms to 205ms, same seeds, same assertions. It also now calls replayFrames instead of hand-rolling the stepping loop it already had a helper for. Deferred deliberately, each with a reason: - Collapsing `prose-nested-marker` into `nested-marker`. Its own docstring names the simplification and defers it; it is a behaviour change and wants its own commit and test, which is the pattern the rest of this branch follows. - Bounding hasSpecialTagMarker on the prose path. Costs tens of microseconds on a long reasoning body, but a marker past the bound would flip that body from released to suppressed — a retraction, which is the bug two commits back. - Splitting the parser out of this `'use client'` file. The strongest structural finding: the file cannot be imported by server code, which is why the inbox executor carries its own thinking-strip regex. It is a mechanical move with no logic change and deserves its own PR, not commit 25 of this one. - Generalising the backtick sanitizer past `workspace_resource`, and replacing it with parser-owned backtick consumption. Same reasoning: real, and not here. Co-Authored-By: Claude Opus 5 (1M context) --- .../special-tags/special-tags.test.ts | 30 ++++++++++++------- .../components/special-tags/special-tags.tsx | 30 +++++++------------ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 2383d1d7fba..5bf12612283 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -823,11 +823,23 @@ describe('parser properties', () => { const pick = (rng: () => number, xs: T[]): T => xs[Math.floor(rng() * xs.length)] - function buildLossless(rng: () => number): string { + function buildLossless(rng: () => number, pool = LOSSLESS_FRAGMENTS): string { const n = 1 + Math.floor(rng() * 5) - return Array.from({ length: n }, () => pick(rng, LOSSLESS_FRAGMENTS)).join('') + return Array.from({ length: n }, () => pick(rng, pool)).join('') } + /** + * The same shapes without the window-crossing filler. + * + * Frame replay parses every prefix, so message length multiplies into parse + * count — the filler fragment alone took that property to ~1M parses and 1.7s, + * 95% of this file's runtime. Retraction is a property of what happens AT a + * frame boundary, so it is exercised by the boundaries, not by message size. + * The scan window still gets its coverage from the other properties, which + * parse each message once. + */ + const SHORT_FRAGMENTS = LOSSLESS_FRAGMENTS.filter((fragment) => fragment.length < 200) + it('never loses a character of a message with nothing droppable in it', () => { // The headline guarantee. Only a well-formed payload that failed its shape // guard may be removed, and no fragment here is one. @@ -876,22 +888,20 @@ describe('parser properties', () => { for (let seed = 1; seed <= 120; seed++) { const rng = makeRng(seed) - const raw = `${buildLossless(rng)}${pick(rng, VALID_TAGS)}${buildLossless(rng)}` + const raw = `${buildLossless(rng, SHORT_FRAGMENTS)}${pick(rng, VALID_TAGS)}${buildLossless(rng, SHORT_FRAGMENTS)}` let previousCards = 0 let previousText = '' - for (let end = 1; end <= raw.length; end += 7) { - const view = visibleView(parseSpecialTags(raw.slice(0, end), true).segments) - - expect(view.cardCount, `seed ${seed} at ${end}: card un-rendered`).toBeGreaterThanOrEqual( + for (const frame of replayFrames(raw, 7)) { + expect(frame.cardCount, `seed ${seed}: card un-rendered`).toBeGreaterThanOrEqual( previousCards ) const stable = previousText.slice(0, Math.max(0, previousText.length - LONGEST_OPENER)) - expect(view.text.startsWith(stable), `seed ${seed} at ${end}: text retracted`).toBe(true) + expect(frame.text.startsWith(stable), `seed ${seed}: text retracted`).toBe(true) - previousCards = view.cardCount - previousText = view.text + previousCards = frame.cardCount + previousText = frame.text } } }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 1b31560f853..83bfb8820ab 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -551,6 +551,11 @@ const MAX_UNCLOSED_BODY_SCAN = 4096 * emitting one would shrink the output and shift every later offset left. */ function blankJsonStringLiterals(body: string): string { + // With no quote there is no string literal, so the loop below would copy the + // body to itself character by character. Both callers reach here on bodies + // that are usually plain prose, and this runs per opener per streamed chunk. + if (!body.includes('"')) return body + let out = '' let inString = false let escaped = false @@ -810,24 +815,11 @@ interface InspectedBody { truncated: boolean } -function inspectWithin(body: string): InspectedBody { - return body.length > MAX_UNCLOSED_BODY_SCAN - ? { text: body.slice(0, MAX_UNCLOSED_BODY_SCAN), truncated: true } - : { text: body, truncated: false } -} - -/** - * {@link inspectWithin} for a body that has not been cut out of the buffer yet. - * - * Slices once, already bounded. Taking `content.slice(bodyStart)` first and - * bounding after copies the entire rest of the message — on every opener, on - * every streamed chunk — and throws all but the window away. - */ -function inspectFrom(content: string, bodyStart: number): InspectedBody { - const end = bodyStart + MAX_UNCLOSED_BODY_SCAN - return end < content.length - ? { text: content.slice(bodyStart, end), truncated: true } - : { text: content.slice(bodyStart), truncated: false } +function inspectWithin(source: string, start = 0): InspectedBody { + const end = start + MAX_UNCLOSED_BODY_SCAN + return end < source.length + ? { text: source.slice(start, end), truncated: true } + : { text: start === 0 ? source : source.slice(start), truncated: false } } /** @@ -972,7 +964,7 @@ function resolveTagAt( const closeIdx = memoizedIndexOf(closeCache, content, closeTag, bodyStart) if (closeIdx === -1) { - const inspected = inspectFrom(content, bodyStart) + const inspected = inspectWithin(content, bodyStart) if (isStreaming && !unclosedTagCannotResolve(tagName, inspected.text)) { return { outcome: 'pending' } } From f59999179ce6798faa0e44e362495afeabb04edb Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:18:49 -0700 Subject: [PATCH 26/27] docs(copilot): reattach an orphaned docstring and cut comment noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One real defect. Adding hasSpecialTagMarker put it BETWEEN unclosedTagCannotResolve and the docstring written for it, so two doc blocks sat stacked and the function they described ended up with none. A reader scanning the file would have read the first block as preamble for the wrong function. Moved back, and while there: "14 substring scans" is SPECIAL_TAG_NAMES.length * 2, so adding an eighth tag would have quietly made it wrong — it now says a pass per tag name. The rest is noise removal: - Three comments narrating what the parser used to do. A comment is read by someone looking at the current code, not the diff, and this branch already writes that history at length in its commit messages. - Three millisecond measurements. Each one already stated the durable claim — quadratic, or a copy thrown away per opener per chunk — and then appended a number that will rot. The one measurement kept is the blind-spot paragraph on MAX_UNCLOSED_BODY_SCAN, where the number is what justifies the constant. - Two of the four restatements of "the renderer concatenates adjacent text segments". Kept where it is load-bearing, on pushText and on the test helper. - One claim gone stale in the last commit: the read-budget doc still described two helpers agreeing with each other, after they became one function. Left alone deliberately: the rules that look arbitrary and are not — marker blanking, the differing resume offsets, the accepted trades. Those are why this file reads as heavily commented, and they are the ones worth having. Co-Authored-By: Claude Opus 5 (1M context) --- .../components/chat-content/chat-sanitize.ts | 3 +- .../special-tags/special-tags.test.ts | 7 -- .../components/special-tags/special-tags.tsx | 70 ++++++++----------- 3 files changed, 31 insertions(+), 49 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts index 606f2523ab8..0c2c336ef3f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts @@ -19,8 +19,7 @@ const HIDDEN_INLINE_REFERENCE_PATTERN = * - **No `` between opener and closer**, via the negative * lookahead. This is a cost bound, not a correctness rule: a lazy scan that may * cross an opener restarts from every opener, so a message repeating the tag - * name is quadratic. Measured at 168KB of such prose: 154ms per call without - * it, 0.34ms with — and this runs on the main thread for every streamed chunk. + * name is quadratic — on the main thread, for every streamed chunk. * * Backticks must be DIRECTLY adjacent. Allowing whitespace between let the * pattern reach past the tag and take the delimiter off a neighbouring code diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts index 5bf12612283..dc40e02cc42 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.ts @@ -223,8 +223,6 @@ describe('parseSpecialTags with ', () => { }) it('still parses a valid tag that follows a rejected one', () => { - // Before the rewrite, rejecting an unclosed tag abandoned the rest of the - // message, so this tag was never parsed at all. const { segments } = parseSpecialTags( 'I use loosely here. Anyway: [{"title":"A","description":"d"}] done.', false @@ -437,8 +435,6 @@ describe('parseSpecialTags with ', () => { }) it('shows prose immediately mid-stream instead of blanking the rest', () => { - // The failure this replaces: everything after the marker stayed invisible - // for the remainder of the stream, then reappeared when it ended. const content = 'The `` chip only renders for a real file.' const { segments, hasPendingTag } = parseSpecialTags(content, true) expect(hasPendingTag).toBe(false) @@ -627,9 +623,6 @@ describe('parseSpecialTags with ', () => { 'The `` file chip only renders when its path points to a real file.' const { segments, hasPendingTag } = parseSpecialTags(content, false) expect(hasPendingTag).toBe(false) - // Asserted on the joined text, not segment boundaries: the renderer - // concatenates adjacent text segments, so how the span is split is not - // observable to a reader. expect(segments.every((segment) => segment.type === 'text')).toBe(true) expect(renderedText(segments)).toBe(content) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 83bfb8820ab..2388ced2c4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -516,10 +516,9 @@ const JSON_BODY_TAG_NAMES: ReadonlySet<(typeof SPECIAL_TAG_NAMES)[number]> = new * first character that breaks JSON viability — so a bounded window reaches the * same verdict as the full remainder for any payload a tag actually carries. * Unbounded, the check is O(body length) and runs once per opener inside a parse - * that re-runs for every streamed chunk: a long reply repeatedly mentioning a tag - * name cost seconds of main-thread time, and one 58KB reply whose early close was - * misspelled — so a single body stretched most of the message — cost 242ms per - * parse against 35ms bounded. + * that re-runs for every streamed chunk. A long reply repeatedly mentioning a + * tag name, or one whose misspelled early close stretches a single body across + * most of the message, is then quadratic in the length of the reply. * * The window's one blind spot, and why it is accepted: a JSON body whose * top-level value closes BEYOND the window, followed by prose and no closing tag, @@ -618,14 +617,24 @@ function isViableJsonPrefixOf(scannable: string): boolean { return true } +/** + * Whether `text` contains a marker for one of the tags this parser knows. + * + * Deliberately the tag NAMES rather than anything tag-shaped. A prose body may + * legitimately contain `
` or `Promise`; only a marker the parser + * would itself act on proves the enclosing opener was text. Shared so the + * streaming and matched-pair paths cannot answer the same question differently + * — them disagreeing is what let a late close swallow content already on screen. + */ +function hasSpecialTagMarker(text: string): boolean { + return SPECIAL_TAG_NAMES.some((name) => text.includes(``) || text.includes(`<${name}>`)) +} + /** * True when an opening tag with no close yet can NEVER resolve, so the text * after it should be shown immediately instead of held back until the stream - * ends. - * - * Without this, a message that merely mentions a tag in prose goes blank from - * that point on for the rest of the stream — the text is only restored once - * streaming stops. + * ends. Without it, a message that merely mentions a tag in prose goes blank + * from that point on until streaming stops. * * One rule decides it, chosen by body kind: * @@ -635,32 +644,18 @@ function isViableJsonPrefixOf(scannable: string): boolean { * prose (no `{` at all), a misspelled close like ``, a * truncated `` or `Promise`; only a marker the parser - * would itself act on proves the enclosing opener was text. Shared so the - * streaming and matched-pair paths cannot answer the same question differently - * — them disagreeing is what let a late close swallow content already on screen. - */ -function hasSpecialTagMarker(text: string): boolean { - return SPECIAL_TAG_NAMES.some((name) => text.includes(``) || text.includes(`<${name}>`)) -} - function unclosedTagCannotResolve( tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string @@ -672,8 +667,7 @@ function unclosedTagCannotResolve( // Cheap rejection before the expensive one. isViableJsonPrefixOf decides on // the first non-whitespace character when it is not `{` or `[` — which is the // common case, a tag name mentioned in prose — so testing it here avoids - // blanking up to a full window of text only to throw the copy away. Measured - // at 86KB of such prose: 43ms per streaming parse before, 2.4ms after. + // blanking up to a full window of text only to throw the copy away. const firstChar = pending.trimStart().charAt(0) if (firstChar !== '' && firstChar !== '{' && firstChar !== '[') return true @@ -804,9 +798,8 @@ export function memoizedIndexOf( * How much of a body may be inspected, and whether that is all of it. * * The read budget, isolated from what the body turns out to BE. Both the - * unclosed and matched-pair paths spend it, and getting them to agree is the - * point: they previously applied the same constant in two shapes, and the - * difference between them was a bug. + * unclosed and matched-pair paths spend it through this one function, so they + * cannot drift out of agreement about how much of a body may be read. */ interface InspectedBody { /** The prefix actually examined. */ @@ -988,9 +981,6 @@ function resolveTagAt( * tags are suppressed and flagged via `hasPendingTag` so the caller can show a * loading indicator, and a trailing partial opening marker (` Date: Mon, 27 Jul 2026 22:35:16 -0700 Subject: [PATCH 27/27] fix(copilot): sanitize backticks in one pass so a flush code span survives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review caught a case the previous fix missed. Two passes each decided independently which backticks belonged together, and with no space between a code span and a tag they disagreed: Open `config.json` ok -> lost the span's closing backtick Open `config.json` ok -> lost the span's opening backtick My test for this used a space between them, which is exactly why it passed. Both passes are now one left-to-right scan alternating between a code span and a tag with a stray backtick against it. A span consumes its own delimiters as the scan reaches them, so a flush neighbour keeps its pair with no special case — where a second pass had no way to know the backtick was already spoken for. This is the third arrangement of this file; the first two each handled the cases they were written for and broke a different one, which is what two passes guessing at the same question produces. A trailing backtick is only taken as a stray when no further backtick follows on the line. Otherwise it is the opener of the next span. Mutation-checked: removing that lookahead fails the new test and nothing else. Swapping the alternation order changes nothing any fixture covers, so the comment no longer claims the order is load-bearing — it distinguishes only a span that opens flush against a tag and closes elsewhere, which nothing pins. Thirteen shapes verified balanced, including all three flush variants, the fenced block, the neighbour with a space, both one-sided strays, and the mention shapes. 168KB of repeated openers: 0.18ms. Co-Authored-By: Claude Opus 5 (1M context) --- .../chat-content/chat-content.test.ts | 18 ++++ .../components/chat-content/chat-sanitize.ts | 91 +++++++++---------- 2 files changed, 63 insertions(+), 46 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts index 652c052b056..fac1e85c77e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts @@ -58,6 +58,24 @@ describe('sanitizeChatDisplayContent', () => { expect(sanitizeChatDisplayContent(content)).toBe(content) }) + it('leaves a code span sitting flush against the tag alone', () => { + // No space between them, so the span's delimiter is directly against the + // tag and a pattern looking for "a backtick beside a tag" cannot tell the + // two apart. A single pass settles it: a span consumes its own delimiters as + // the scan reaches them, and a trailing backtick is only taken as a stray + // when no further backtick follows on the line. + const before = + 'Open `config.json`{"type":"file","path":"a.md","title":"a"} ok' + const after = + 'Open {"type":"file","path":"a.md","title":"a"}`config.json` ok' + const both = + 'Open `a`{"type":"file","path":"a.md","title":"a"}`b` ok' + + expect(sanitizeChatDisplayContent(before)).toBe(before) + expect(sanitizeChatDisplayContent(after)).toBe(after) + expect(sanitizeChatDisplayContent(both)).toBe(both) + }) + it('does not break the closing fence of a code block containing a tag', () => { // Whitespace matching used to cross the newline and consume one of the three // closing backticks, so the block never closed and the rest of the message diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts index 0c2c336ef3f..4e0792df0ab 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize.ts @@ -2,63 +2,62 @@ const HIDDEN_INLINE_REFERENCE_PATTERN = /`[^`\n]*(?:internal\/tool-results\/|internal\/blocktips\/|components\/integrations\/[^`\n]*README)[^`\n]*`/g /** - * A complete workspace-resource tag, with any backtick sitting directly against - * either end. Replacing with the tag alone drops those backticks, which is what - * lets the chip render when the model wrapped the tag in a code span. + * A complete workspace-resource tag: opener, payload, closer. * - * Three constraints, each load-bearing: + * Two constraints on the payload, both load-bearing: * - * - **The closer must be present.** Prose MENTIONING the tag name writes - * `` with no closer at all. Stripping a mention's opening - * backtick leaves the closing one unpaired, and it opens a code span that runs - * to the next backtick — inverting every code span in the rest of the message. - * - **No backtick between opener and closer.** A payload is JSON and carries - * none, while a message explaining the syntax writes the opener and the closer - * as two separately backticked spans. Without this, that prose reads as one - * wrapped tag and loses its outer pair. - * - **No `` between opener and closer**, via the negative - * lookahead. This is a cost bound, not a correctness rule: a lazy scan that may - * cross an opener restarts from every opener, so a message repeating the tag - * name is quadratic — on the main thread, for every streamed chunk. - * - * Backticks must be DIRECTLY adjacent. Allowing whitespace between let the - * pattern reach past the tag and take the delimiter off a neighbouring code - * span — `` Open `config.json` `` lost a backtick that way, and `\s*` - * crossing a newline broke the closing fence of a code block containing a tag. + * - **No backtick.** A payload is JSON and carries none, so this is what tells a + * real tag from prose MENTIONING the tag name — a message explaining the + * syntax writes the opener and the closer as two separately backticked spans. + * - **No nested opener**, via the negative lookahead. A cost bound rather than a + * correctness rule: a lazy scan allowed to cross an opener restarts from every + * opener, so a message repeating the tag name is quadratic — on the main + * thread, for every streamed chunk. * * Accepted trade: a resource whose title or path itself contains a backtick is * not matched, so it renders as text rather than a chip. That costs one chip and * is rare; the failure it replaces corrupts a whole message and is common. */ -const WORKSPACE_RESOURCE_TAG_WITH_BACKTICKS = - /`?((?:(?!)[^`])*?<\/workspace_resource>)`?/g +const COMPLETE_TAG_SOURCE = + '(?:(?!)[^`])*?<\\/workspace_resource>' + +/** Non-global so {@link RegExp.test} has no `lastIndex` to carry between calls. */ +const COMPLETE_WORKSPACE_RESOURCE_TAG = new RegExp(COMPLETE_TAG_SOURCE) /** - * An inline code span, paired the way markdown pairs one: opening backtick to - * the next backtick on the same line. + * One left-to-right pass over the two things that can own a backtick: an inline + * code span, and a tag with a stray backtick pressed against it. + * + * ONE pass is the design. Two separate passes each have to guess which backticks + * belong together, and every previous arrangement of this file got a different + * case wrong — a span two words away, a code fence, then a span sitting flush + * against the tag. Here a span consumes its own delimiters as the scan reaches + * them, so `` `config.json` `` keeps its pair without a special case. * - * Pairing matters. A pattern that instead looked for "a backtick, then a tag, - * then a backtick" would happily start at one span's delimiter and end at a - * different span's, unwrapping the text between two unrelated spans — which is - * how `` Open `config.json` then run `bun test` `` lost a backtick. + * The trailing backtick is only taken when no further backtick follows on the + * line; otherwise it is not a stray at all but the opener of the next span, and + * `` `config.json` `` would lose that span's delimiter. A LEADING backtick + * needs no such guard, because a backtick that closes a span is consumed as part + * of that span. Of the two, only the trailing lookahead is pinned by a test — + * swapping the alternatives changes behaviour only for a span that both opens + * flush against a tag and closes elsewhere, which no fixture covers. */ -const CODE_SPAN_PATTERN = /`([^`\n]*)`/g - -/** Non-global so {@link RegExp.test} has no `lastIndex` to carry between calls. */ -const COMPLETE_WORKSPACE_RESOURCE_TAG = - /(?:(?!)[^`])*?<\/workspace_resource>/ +const CODE_SPAN_OR_FLANKED_TAG = new RegExp( + `\`[^\`\\n]*\`|\`?(${COMPLETE_TAG_SOURCE})(?:\`(?![^\`\\n]*\`))?`, + 'g' +) export function sanitizeChatDisplayContent(content: string): string { - return ( - content - // A tag inside a code span: drop the delimiters, keep the content. The - // parser extracts the tag either way, so leaving them would strand a pair - // of backticks around a hole once the chip is lifted out. - .replace(CODE_SPAN_PATTERN, (span, inner: string) => - COMPLETE_WORKSPACE_RESOURCE_TAG.test(inner) ? inner : span - ) - .replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') - // A leftover backtick pressed against a tag, with no partner to pair with. - .replace(WORKSPACE_RESOURCE_TAG_WITH_BACKTICKS, '$1') - ) + return content + .replace(CODE_SPAN_OR_FLANKED_TAG, (match, tag?: string) => { + // A tag with stray backticks against it: keep the tag, drop the strays. + if (tag !== undefined) return tag + + // A code span. Unwrap it only when it genuinely holds a tag — the parser + // lifts the tag out either way, so leaving the delimiters would strand a + // pair of backticks around a hole. Anything else is someone else's span. + const inner = match.slice(1, -1) + return COMPLETE_WORKSPACE_RESOURCE_TAG.test(inner) ? inner : match + }) + .replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') }