` 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(`${name}>`) || 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(`${name}>`) || 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/34] 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, '')
}
From 2a4c0f4c1a877676703924d6c05eec1b1c84d084 Mon Sep 17 00:00:00 2001
From: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Date: Tue, 28 Jul 2026 19:24:34 -0700
Subject: [PATCH 28/34] fix(copilot): rewind the scan-window resume so a
straddling opener still parses
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Applied from an automated code review of this branch. Two findings, both
verified by reverting the fix and confirming only the new test fails.
`resumeForClass`'s `unexamined` case resumed at exactly `bodyStart + 4096`.
That edge is an arbitrary cut, so an opener can begin just before it and
finish just after — leaving its `<` behind the cursor. The opener scan only
looks forward, so the tag was never found and its payload rendered as raw
JSON text on a COMPLETED message, not just mid-stream. Reproducible for
every filler length in 4077..4095; the existing borrowed-body test steps in
11-character units and never lands in that band.
Backing the resume off by the longest marker guarantees a straddling opener
is re-scanned from its `<`. The step is still ~4076 characters, so a long
body still costs a bounded number of re-entries. The new test sweeps every
offset across the band.
The two complexity tests asserted absolute wall-clock ceilings (<50ms,
<20ms), which measure the machine as much as the algorithm: they fail on a
loaded CI box, and set generously enough not to, they let a genuine
quadratic through at the single size they sample. Both now assert the
scaling ratio across a 4x input instead (quadratic ~16x, linear ~4x).
Co-Authored-By: Claude Opus 5 (1M context)
---
.../chat-content/chat-content.test.ts | 30 +++++++++---
.../special-tags/special-tags.test.ts | 47 +++++++++++++++++--
.../components/special-tags/special-tags.tsx | 24 ++++++++--
3 files changed, 86 insertions(+), 15 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 fac1e85c77e..b309a917087 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
@@ -89,13 +89,29 @@ describe('sanitizeChatDisplayContent', () => {
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)
+ // for every streamed chunk.
+ //
+ // Asserted as a RATIO across a 4x input rather than a wall-clock ceiling. A
+ // fixed millisecond bound measures the machine as much as the algorithm: it
+ // fails on a loaded CI box, and set generously enough not to, it lets a
+ // genuine quadratic through at the single size it happens to sample.
+ // Quadratic costs ~16x for 4x the input; linear costs ~4x.
+ const build = (times: number) => 'The tag is used here. '.repeat(times)
+ const fastest = (content: string) => {
+ let best = Number.POSITIVE_INFINITY
+ for (let run = 0; run < 5; run++) {
+ const startedAt = performance.now()
+ sanitizeChatDisplayContent(content)
+ best = Math.min(best, performance.now() - startedAt)
+ }
+ return best
+ }
+
+ fastest(build(2_000))
+ const small = fastest(build(2_000))
+ const large = fastest(build(8_000))
+
+ expect(large / small).toBeLessThan(8)
})
it('still unwraps a real tag that carries a stray backtick on one side only', () => {
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 dc40e02cc42..23b2ffc2053 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
@@ -427,6 +427,30 @@ describe('parseSpecialTags with ', () => {
}
})
+ it('still finds a valid tag whose opener STRADDLES the scan-window edge', () => {
+ // The window edge is an arbitrary cut, so an opener can begin just before it
+ // and finish just after. The test above steps in 11-character units and so
+ // lands on only a few offsets; the straddle needs every offset in the band.
+ //
+ // Resuming exactly at the edge left the opener's `<` behind the cursor, and
+ // the opener scan only looks FORWARD — so the tag was never found and its
+ // payload rendered as raw JSON text, on a COMPLETED message. Every offset
+ // across the band must still produce the card.
+ const inner =
+ '{"type":"file","path":"files/b.md","title":"b.md"}'
+
+ for (let filler = 4_060; filler <= 4_110; filler++) {
+ const raw = `See ${'z'.repeat(filler)}${inner} end`
+ const { segments } = parseSpecialTags(raw, false)
+ expect(
+ segments.filter((segment) => segment.type === 'workspace_resource'),
+ `filler ${filler}`
+ ).toHaveLength(1)
+ // Nothing is duplicated or dropped by the rewind either.
+ expect(renderedText(segments), `filler ${filler}`).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'
@@ -541,11 +565,26 @@ describe('parseSpecialTags with ', () => {
// 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)
+ //
+ // Asserted as a RATIO across a 4x input rather than a wall-clock ceiling, so
+ // the test pins the complexity instead of the speed of the machine running
+ // it. Quadratic costs ~16x for 4x the input; linear costs ~4x.
+ const build = (times: number) => 'The tag is used here. '.repeat(times)
+ const fastest = (content: string) => {
+ let best = Number.POSITIVE_INFINITY
+ for (let run = 0; run < 5; run++) {
+ const startedAt = performance.now()
+ parseSpecialTags(content, true)
+ best = Math.min(best, performance.now() - startedAt)
+ }
+ return best
+ }
+
+ fastest(build(2_000))
+ const small = fastest(build(2_000))
+ const large = fastest(build(8_000))
- const startedAt = performance.now()
- parseSpecialTags(content, true)
- expect(performance.now() - startedAt).toBeLessThan(20)
+ expect(large / small).toBeLessThan(8)
})
it('does not let a late thinking close swallow content already on screen', () => {
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 2388ced2c4d..75400744d4d 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
@@ -533,6 +533,15 @@ const JSON_BODY_TAG_NAMES: ReadonlySet<(typeof SPECIAL_TAG_NAMES)[number]> = new
*/
const MAX_UNCLOSED_BODY_SCAN = 4096
+/**
+ * Length of the longest marker the scans can match.
+ *
+ * Derived from {@link SPECIAL_TAG_NAMES} rather than hand-counted, so adding a
+ * longer tag name cannot silently shrink the rewind in {@link resumeForClass}.
+ * Closing markers are the longer of the two forms, so they set the bound.
+ */
+const LONGEST_TAG_MARKER = Math.max(...SPECIAL_TAG_NAMES.map((name) => `${name}>`.length))
+
/**
* Strip the contents of JSON string literals from `body`, replacing them with
* spaces so every other index is preserved.
@@ -913,10 +922,17 @@ function resumeForClass(cls: BodyClass, bodyStart: number, pastClose: number): n
// 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
+ // Resume just short of the first character NOT read. Everything read is
+ // emitted as text by the caller, and this advances nearly a full window per
+ // step, so a long body still costs a bounded number of re-entries.
+ //
+ // The rewind is load-bearing: the window edge is an arbitrary cut, so an
+ // opener can straddle it. Resuming exactly at the edge leaves that opener's
+ // `<` behind the cursor, and the opener scan only looks FORWARD — so the tag
+ // is never found and renders as raw payload text, on a completed message.
+ // Backing off by the longest marker guarantees any straddling opener is
+ // re-scanned from its `<`.
+ return bodyStart + MAX_UNCLOSED_BODY_SCAN - (LONGEST_TAG_MARKER - 1)
}
}
From 1943cfe84f27f4ac4e9867128aacbee6e298d534 Mon Sep 17 00:00:00 2001
From: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Date: Tue, 28 Jul 2026 21:27:42 -0700
Subject: [PATCH 29/34] fix(copilot): trust the raw body for markers once it is
known not to be JSON
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`literalTextReason` blanks a body's quoted regions before scanning it for tag
markers, so that syntax quoted inside a JSON string is not mistaken for a real
nested tag. That blanking assumes quotes delimit JSON strings. One unbalanced
`"` breaks the assumption: everything after it is treated as string content,
which can hide a genuine marker.
The verdict then degrades from `foreign-markers` to `never-a-payload`, and the
resume changes with it — from the marker offset to past the close — flattening
a real tag inside the span. A card already on screen un-renders into raw JSON
when the closing tag finally arrives, and a valid tag after it never renders.
Blanking is only meaningful while the body might BE JSON. Once viability or a
failed parse has proved it never was, that premise is void and the raw text is
the honest evidence, so rescan it and resume at the marker.
Both routes to "never JSON" now funnel through one branch. The rescan applies
to the failed-parse route too, not only the viability one — patching just the
latter leaves the same defect reachable through the former.
Behaviour for a body that IS valid JSON is untouched: a well-formed payload
that fails its shape guard is still discarded, and tag syntax quoted inside a
valid payload is still invisible to the scan.
Adds the repro as two tests — the settled parse, and frame-by-frame so the
un-render is pinned directly — plus two unbalanced-quote fragments to the
property corpus. Reverting the rescan fails exactly those two tests.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../special-tags/special-tags.test.ts | 48 +++++++++++++++++++
.../components/special-tags/special-tags.tsx | 21 ++++++--
2 files changed, 66 insertions(+), 3 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 23b2ffc2053..c5589f53d17 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
@@ -353,6 +353,52 @@ describe('parseSpecialTags with ', () => {
expect(renderedText(segments)).toContain('')
})
+ it('finds a nested tag an unbalanced quote hid from the blanked scan', () => {
+ // One stray `"` is enough to make blankJsonStringLiterals treat the REST of
+ // the body as a string literal, hiding the real `` marker from the
+ // scan. The verdict then degrades from `foreign-markers` to `never-a-payload`
+ // and resumes past the close, flattening both nested tags into one literal
+ // span — so a card already on screen un-renders when the close arrives.
+ //
+ // Blanking is only meaningful while the body might BE json. Once viability
+ // has proved it never was, the raw text is the honest evidence.
+ const raw =
+ 'Saved the notes file "notes.md and here is what to do next: ' +
+ '[{"title":"Ship it","description":"Open the PR"}]\n' +
+ 'Full path: {"type":"file","path":"files/a.md","title":"a.md"}'
+
+ const { segments } = parseSpecialTags(raw, false)
+ expect(segments.filter((segment) => segment.type === 'options')).toHaveLength(1)
+ expect(segments.filter((segment) => segment.type === 'workspace_resource')).toHaveLength(1)
+ // Balancing the quote must reach the same two cards — the quote is the only
+ // difference, so this pins that it was never load-bearing for the outcome.
+ const balanced = raw.replace('the notes file "notes.md', 'the notes file notes.md')
+ const control = parseSpecialTags(balanced, false).segments
+ expect(control.filter((segment) => segment.type === 'options')).toHaveLength(1)
+ expect(control.filter((segment) => segment.type === 'workspace_resource')).toHaveLength(1)
+ })
+
+ it('never un-renders that card as the closing tag arrives', () => {
+ // The frame-level face of the case above, and the invariant it broke: the
+ // options card is on screen for many frames before the final `>` lands. A
+ // card that renders must never revert to raw text.
+ const raw =
+ 'Saved the notes file "notes.md and here is what to do next: ' +
+ '[{"title":"Ship it","description":"Open the PR"}]\n' +
+ 'Full path: {"type":"file","path":"files/a.md","title":"a.md"}'
+
+ let sawCard = false
+ for (let end = 1; end <= raw.length; end++) {
+ const { segments } = parseSpecialTags(raw.slice(0, end), true)
+ const hasCard = segments.some((segment) => segment.type === 'options')
+ if (hasCard) sawCard = true
+ expect(!sawCard || hasCard, `options card retracted at frame ${end}`).toBe(true)
+ }
+ expect(sawCard).toBe(true)
+ // ...and the settled parse still has it.
+ expect(parseSpecialTags(raw, false).segments.some((s) => s.type === 'options')).toBe(true)
+ })
+
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
@@ -849,6 +895,8 @@ describe('parser properties', () => {
"{'type':'file'} single quotes. ",
'the gmail-agent workflow prose body. ',
'reasoning with a nested marker after. ',
+ 'the notes file "notes.md unbalanced quote after. ',
+ 'notes "unbalanced then marker tail. ',
'\n\nA paragraph break above. ',
`${'long filler prose. '.repeat(300)}crossing the scan window. `,
]
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 75400744d4d..8941495e7ae 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
@@ -887,13 +887,28 @@ function classifyBody(tagName: (typeof SPECIAL_TAG_NAMES)[number], body: string)
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' }
+ // parse can. Both routes to that answer are funnelled through one place so the
+ // rescan below cannot be added to one and forgotten on the other.
+ const neverJson =
+ verdict?.reason === 'never-a-payload' || (isJsonBodied && !isParseableJson(body))
+
+ if (neverJson) {
+ // literalTextReason blanked this body's quoted regions on the assumption it
+ // was JSON. It never was, so that assumption is void — and a body with an
+ // odd number of `"` blanks the WRONG regions, which can hide a real marker
+ // and turn what should be `nested-marker` into `never-json`. The difference
+ // is not academic: `never-json` resumes past the close, flattening a genuine
+ // tag inside the span, so a card already on screen un-renders when the close
+ // finally arrives. With the JSON premise gone, the raw text is the honest
+ // evidence, and a marker in it means the close we matched belongs elsewhere.
+ const rawMarker = TAG_SHAPED_MARKER.exec(inspected.text)
+ if (rawMarker) return { kind: 'nested-marker', offsetInBody: rawMarker.index }
+ return { kind: 'never-json' }
+ }
return { kind: 'broken-payload' }
}
From 5066af168ea43e9b2b09465003f7f0ebfdf722f6 Mon Sep 17 00:00:00 2001
From: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Date: Tue, 28 Jul 2026 22:22:02 -0700
Subject: [PATCH 30/34] refactor(copilot): share the scaling-ratio harness
between the two perf tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both complexity tests hand-rolled the same 15-line harness — build a repeated
tag mention, take the fastest of five runs at two input sizes, assert the
ratio — differing only in which function they timed. Changing the run count,
the sample sizes, or the threshold meant editing both in lockstep.
Extracted to `scalingRatioOver4x`, following the existing `*-test-helpers.ts`
convention in this tree. The rationale for asserting a ratio rather than a
wall-clock ceiling now lives in one place instead of being paraphrased twice.
Also drops a redundant disjunct in the opener scan: `nearestStart` and
`nearestTagName` are only ever assigned together, so `nearestStart === -1`
holds exactly when the name is empty. Testing the name alone is the same check
and is the one that narrows the union for `resolveTagAt`.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../chat-content/chat-content.test.ts | 25 ++----------
.../components/scaling-test-helpers.ts | 39 +++++++++++++++++++
.../special-tags/special-tags.test.ts | 23 ++---------
.../components/special-tags/special-tags.tsx | 5 ++-
4 files changed, 51 insertions(+), 41 deletions(-)
create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/scaling-test-helpers.ts
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 b309a917087..1b3e5d105d9 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
@@ -2,6 +2,7 @@
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
+import { scalingRatioOver4x } from '@/app/workspace/[workspaceId]/home/components/message-content/components/scaling-test-helpers'
import { sanitizeChatDisplayContent } from './chat-sanitize'
describe('sanitizeChatDisplayContent', () => {
@@ -91,27 +92,9 @@ describe('sanitizeChatDisplayContent', () => {
// is quadratic — 154ms for this input before the bound, on the main thread,
// for every streamed chunk.
//
- // Asserted as a RATIO across a 4x input rather than a wall-clock ceiling. A
- // fixed millisecond bound measures the machine as much as the algorithm: it
- // fails on a loaded CI box, and set generously enough not to, it lets a
- // genuine quadratic through at the single size it happens to sample.
- // Quadratic costs ~16x for 4x the input; linear costs ~4x.
- const build = (times: number) => 'The tag is used here. '.repeat(times)
- const fastest = (content: string) => {
- let best = Number.POSITIVE_INFINITY
- for (let run = 0; run < 5; run++) {
- const startedAt = performance.now()
- sanitizeChatDisplayContent(content)
- best = Math.min(best, performance.now() - startedAt)
- }
- return best
- }
-
- fastest(build(2_000))
- const small = fastest(build(2_000))
- const large = fastest(build(8_000))
-
- expect(large / small).toBeLessThan(8)
+ // Asserted as a scaling ratio, not a wall-clock ceiling — see
+ // {@link scalingRatioOver4x} for why.
+ expect(scalingRatioOver4x((content) => sanitizeChatDisplayContent(content))).toBeLessThan(8)
})
it('still unwraps a real tag that carries a stray backtick on one side only', () => {
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/scaling-test-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/scaling-test-helpers.ts
new file mode 100644
index 00000000000..7d2dab3b83e
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/scaling-test-helpers.ts
@@ -0,0 +1,39 @@
+/**
+ * Repeated mention of a tag name that never closes — the shape both the parser
+ * and the display sanitizer used to be quadratic on, because a scan allowed to
+ * cross an opener restarts from every opener.
+ */
+function buildRepeatedTagMentions(times: number): string {
+ return 'The tag is used here. '.repeat(times)
+}
+
+/** Fastest of five runs, so a single scheduling hiccup cannot skew the sample. */
+function fastest(run: (content: string) => void, content: string): number {
+ let best = Number.POSITIVE_INFINITY
+ for (let attempt = 0; attempt < 5; attempt++) {
+ const startedAt = performance.now()
+ run(content)
+ best = Math.min(best, performance.now() - startedAt)
+ }
+ return best
+}
+
+/**
+ * How much slower `run` gets when its input grows 4x.
+ *
+ * Complexity is asserted as a RATIO rather than a wall-clock ceiling. A fixed
+ * millisecond bound measures the machine as much as the algorithm: it fails on a
+ * loaded CI box, and set generously enough not to, it lets a genuine quadratic
+ * through at the single size it happens to sample. Quadratic costs ~16x for 4x
+ * the input; linear costs ~4x.
+ */
+export function scalingRatioOver4x(run: (content: string) => void): number {
+ // Warm up first — the JIT would otherwise charge the whole compile to the
+ // small sample and flatter the ratio.
+ fastest(run, buildRepeatedTagMentions(2_000))
+
+ const small = fastest(run, buildRepeatedTagMentions(2_000))
+ const large = fastest(run, buildRepeatedTagMentions(8_000))
+
+ return large / small
+}
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 c5589f53d17..4ae4b479343 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,6 +14,7 @@ vi.mock('@/lib/auth/auth-client', () => ({
useSession: vi.fn(() => ({ data: null, isPending: false })),
}))
+import { scalingRatioOver4x } from '@/app/workspace/[workspaceId]/home/components/message-content/components/scaling-test-helpers'
import type {
ContentSegment,
IndexOfCache,
@@ -612,25 +613,9 @@ describe('parseSpecialTags with ', () => {
// or `[` — the common case. Testing that before blanking avoids copying a
// full window per opener per chunk: 43ms to 2ms on this input.
//
- // Asserted as a RATIO across a 4x input rather than a wall-clock ceiling, so
- // the test pins the complexity instead of the speed of the machine running
- // it. Quadratic costs ~16x for 4x the input; linear costs ~4x.
- const build = (times: number) => 'The tag is used here. '.repeat(times)
- const fastest = (content: string) => {
- let best = Number.POSITIVE_INFINITY
- for (let run = 0; run < 5; run++) {
- const startedAt = performance.now()
- parseSpecialTags(content, true)
- best = Math.min(best, performance.now() - startedAt)
- }
- return best
- }
-
- fastest(build(2_000))
- const small = fastest(build(2_000))
- const large = fastest(build(8_000))
-
- expect(large / small).toBeLessThan(8)
+ // Asserted as a scaling ratio, not a wall-clock ceiling — see
+ // {@link scalingRatioOver4x} for why.
+ expect(scalingRatioOver4x((content) => parseSpecialTags(content, true))).toBeLessThan(8)
})
it('does not let a late thinking close swallow content already on screen', () => {
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 8941495e7ae..3393b4190af 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
@@ -1042,7 +1042,10 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS
}
}
- if (nearestStart === -1 || nearestTagName === '') {
+ // Only the name is tested: the two are assigned together above, so an empty
+ // name and a -1 start are the same state — and the name is the one that
+ // needs narrowing before resolveTagAt below.
+ if (nearestTagName === '') {
let remaining = content.slice(cursor)
if (isStreaming) {
From 29722096033affd151587d4d8d871ccc909726f7 Mon Sep 17 00:00:00 2001
From: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Date: Tue, 28 Jul 2026 22:33:59 -0700
Subject: [PATCH 31/34] docs(copilot): correct a resume comment the rewind fix
left inaccurate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The `unexamined` case claimed "everything read is emitted as text by the
caller". That was true when the resume was exactly the window edge, but the
straddling-opener rewind holds the last marker's worth of the window back so it
can be re-scanned rather than flattened — so the sentence contradicted the
paragraph directly beneath it. Nothing is lost either way; the caller emits up
to wherever this resumes.
Also drops two "Round-N class:" prefixes from test comments. They point at
review rounds of this PR, which mean nothing to a reader after it merges; the
sentences that follow already say what the bug was.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../components/special-tags/special-tags.test.ts | 4 ++--
.../components/special-tags/special-tags.tsx | 9 ++++++---
2 files changed, 8 insertions(+), 5 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 4ae4b479343..4191a60072e 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
@@ -916,7 +916,7 @@ describe('parser properties', () => {
})
it('renders every valid tag as a card whatever surrounds it', () => {
- // Round-3 class: a valid tag was flattened to text purely because of how much
+ // A valid tag was flattened to text purely because of how much
// prose preceded it, which no fixed example set would have found.
for (let seed = 1; seed <= 400; seed++) {
const rng = makeRng(seed)
@@ -943,7 +943,7 @@ describe('parser properties', () => {
})
it('never un-renders a card or retracts text across streamed frames', () => {
- // Round-5 class: content already on screen disappeared when a later close
+ // Content already on screen disappeared when a later close
// arrived. Only visible across frames, never in an end-state assertion.
//
// Text may shrink slightly at a frame edge: a half-arrived opening marker is
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 3393b4190af..c9836de8c5c 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
@@ -937,9 +937,12 @@ function resumeForClass(cls: BodyClass, bodyStart: number, pastClose: number): n
// Rescan the whole body: nothing was blanked, so no marker is hidden.
return bodyStart
case 'unexamined':
- // Resume just short of the first character NOT read. Everything read is
- // emitted as text by the caller, and this advances nearly a full window per
- // step, so a long body still costs a bounded number of re-entries.
+ // Resume just short of the first character NOT read: the last marker's
+ // worth of the window is held back rather than emitted as text, so it is
+ // re-scanned on the next pass instead of being flattened. Nothing is lost
+ // — the caller emits up to wherever this resumes. It still advances nearly
+ // a full window per step, so a long body costs a bounded number of
+ // re-entries.
//
// The rewind is load-bearing: the window edge is an arbitrary cut, so an
// opener can straddle it. Resuming exactly at the edge leaves that opener's
From 540667aab0f0b3b6d0c6231fef56d25f558788af Mon Sep 17 00:00:00 2001
From: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Date: Tue, 28 Jul 2026 22:46:35 -0700
Subject: [PATCH 32/34] test(copilot): pin the blanking rule with a body
staging cannot recover
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CI runs the merge with staging, and staging's desktop PR (#5998) taught
`parseSpecialTagData` to recover a failed `` body's prompt and render
it as text instead of returning null. That recovery lands before `classifyBody`
is ever consulted, so this fixture — whose quoted `` sat inside its
`prompt` — stopped exercising the blanking rule and started asserting the
recovery. Merged, it rendered "A use here? B" instead of "A B".
Moving the quoted marker to a non-`prompt` field restores what the test is for:
a marker inside a JSON string must be blanked before the scan, or a broken
payload gets classified as literal text and its raw JSON is shown. A body with
no recoverable prompt reaches `discard` on both sides of the merge, matching the
prompt-less fixture the sibling test above already uses.
Behaviour is unchanged on either branch alone; only the fixture moved.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../components/special-tags/special-tags.test.ts | 13 +++++++++----
1 file changed, 9 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 4191a60072e..2f2bcb83a12 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
@@ -303,11 +303,16 @@ describe('parseSpecialTags with ', () => {
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.
+ // Scanning the raw body sees `` inside the payload, calls the span
+ // literal text, and renders the raw JSON — the outcome `discard` exists to
+ // prevent.
+ //
+ // The quoted marker deliberately sits in a field OTHER than `prompt`: a
+ // recoverable prompt is surfaced as text before this path is reached, so a
+ // fixture carrying one would assert the recovery rather than the blanking
+ // this test exists for. Matches the prompt-less body used above.
const { segments } = parseSpecialTags(
- 'A [{"type":"single_select","prompt":"use here?"}] B',
+ 'A [{"type":"single_select","title":"use here?"}] B',
false
)
expect(renderedText(segments)).toBe('A B')
From 47e1f2f9daac75d645a98c6bd5d28de7b90e432b Mon Sep 17 00:00:00 2001
From: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Date: Tue, 28 Jul 2026 23:05:54 -0700
Subject: [PATCH 33/34] test(copilot): cover every tag in the property
invariants, and pin that it stays that way
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`VALID_TAGS` and `NEEDLES` hand-picked three or four of the seven tags the
parser resolves. `credential`, `usage_upgrade`, and `mothership-error` were
exercised by no invariant at all — not the card-count property, not
retraction-across-frames, not settled-vs-last-frame — and nothing failed to say
so. The property block's whole argument is that it covers combinations no fixed
example set would, which was only true for four sevenths of the tag space.
Fixtures are now keyed by tag name and checked against SPECIAL_TAG_NAMES, so
adding a tag without a fixture fails a test instead of quietly falling outside
every property here. `thinking` is listed separately: it renders nothing, so it
cannot carry a card invariant.
SPECIAL_TAG_NAMES is exported for that check, matching the five sibling
`*_TYPES` unions in the same module that are already exported. NEEDLES derives
from it too, so the memoization test searches every opener the parser does.
All three newly covered tags pass every invariant — this closes a coverage gap,
it does not fix a bug. Removing any one fixture fails the new guard.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../special-tags/special-tags.test.ts | 43 ++++++++++++++++---
.../components/special-tags/special-tags.tsx | 7 ++-
2 files changed, 43 insertions(+), 7 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 2f2bcb83a12..66b5c599b75 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
@@ -23,6 +23,7 @@ import {
memoizedIndexOf,
parseQuestionTagBody,
parseSpecialTags,
+ SPECIAL_TAG_NAMES,
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags'
/**
@@ -797,7 +798,10 @@ describe('memoizedIndexOf', () => {
const CONTENT =
'Use for files. Use for cards. ' +
'[{"title":"Ship","description":"go"}] and again.'
- const NEEDLES = ['', '', '', '']
+ // Every opener the parser actually searches for, not a hand-picked few: the
+ // cache is keyed per needle, so a needle absent from CONTENT exercises the
+ // cached -1 path and a repeated one exercises reuse as the cursor advances.
+ const NEEDLES = SPECIAL_TAG_NAMES.map((name) => `<${name}>`)
it('matches plain indexOf as the cursor advances', () => {
const cache: IndexOfCache = new Map()
@@ -861,11 +865,38 @@ describe('parser properties', () => {
}
}
- const VALID_TAGS = [
- '{"type":"file","path":"files/a.md","title":"a.md"}',
- '[{"title":"Ship it","description":"Open the PR"}]',
- `${JSON.stringify(SINGLE_SELECT)}`,
- ]
+ /**
+ * One valid payload per card-rendering tag, keyed by tag name so
+ * {@link SPECIAL_TAG_NAMES} can be checked for full coverage below. Hand-picking
+ * a subset is how three of these went unexercised by every invariant without
+ * anything failing to say so.
+ */
+ const VALID_TAG_BY_NAME: Record = {
+ workspace_resource:
+ '{"type":"file","path":"files/a.md","title":"a.md"}',
+ options: '[{"title":"Ship it","description":"Open the PR"}]',
+ question: `${JSON.stringify(SINGLE_SELECT)}`,
+ credential:
+ '{"type":"link","provider":"slack","value":"https://x.example/p"}',
+ usage_upgrade:
+ '{"reason":"monthly cap","action":"upgrade_plan","message":"You hit your limit."}',
+ 'mothership-error':
+ '{"message":"The tool call failed.","code":"E_TOOL"}',
+ }
+
+ /** Renders nothing rather than a card, so it cannot carry a card invariant. */
+ const TAGS_WITHOUT_CARDS = ['thinking']
+
+ const VALID_TAGS = Object.values(VALID_TAG_BY_NAME)
+
+ it('covers every tag the parser knows', () => {
+ // The invariants below are only as good as this list. Deriving the check from
+ // SPECIAL_TAG_NAMES makes adding a tag without a fixture fail loudly here,
+ // instead of quietly leaving it outside every property in this file.
+ expect(new Set([...Object.keys(VALID_TAG_BY_NAME), ...TAGS_WITHOUT_CARDS])).toEqual(
+ new Set(SPECIAL_TAG_NAMES)
+ )
+ })
/**
* Fragments that must survive verbatim. Every one is a shape the parser has 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 c9836de8c5c..65a18c32da6 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
@@ -196,7 +196,12 @@ const RUNTIME_SPECIAL_TAG_NAMES = [
'question',
] as const
-const SPECIAL_TAG_NAMES = [
+/**
+ * Every tag the parser resolves. Exported so tests can assert their fixtures
+ * cover all of them rather than hand-picking a subset that silently drifts —
+ * the same treatment the sibling `*_TYPES` unions above already get.
+ */
+export const SPECIAL_TAG_NAMES = [
'thinking',
'options',
'usage_upgrade',
From 6fbd3992d0ed042bc1f562bd84e99b017c1781ca Mon Sep 17 00:00:00 2001
From: Justin Blumencranz <96924014+j15z@users.noreply.github.com>
Date: Tue, 28 Jul 2026 23:19:18 -0700
Subject: [PATCH 34/34] docs(copilot): state the actual bound on the thinking
nesting rule
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The docstring implied generics are excluded from the marker check. They are not:
the match is a substring test, so `Promise` is safe only because `void` is
not a tag name, while `Promise` does match and would release a thinking
body as text.
Says so plainly now, with why it is left as-is: the boundary check that would
narrow it wants a lookbehind, which is Safari 16.4+ and would be a parse-time
SyntaxError on the versions this app still supports — a dead client chunk is a
worse outcome than the bug. Reaching it also needs an inline `` body,
which the agent no longer emits, discussing a type named exactly after a tag.
Comment only; no behaviour change.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../components/special-tags/special-tags.tsx | 7 +++++++
1 file changed, 7 insertions(+)
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 65a18c32da6..28207435d56 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
@@ -639,6 +639,13 @@ function isViableJsonPrefixOf(scannable: string): boolean {
* 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.
+ *
+ * The match is by substring, so a generic is safe only when its parameter is not
+ * itself a tag name: `Promise` does not match, `Promise` does. The
+ * narrowing is not worth its cost — it needs a `` body, which the agent
+ * no longer emits (reasoning arrives as structured thinking blocks), discussing a
+ * type named exactly after a tag; and the boundary check that would fix it wants a
+ * lookbehind, unavailable on the Safari versions this app still supports.
*/
function hasSpecialTagMarker(text: string): boolean {
return SPECIAL_TAG_NAMES.some((name) => text.includes(`${name}>`) || text.includes(`<${name}>`))