` 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/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, '')
}