Skip to content

Commit bad05fe

Browse files
j15zclaude
andcommitted
fix(copilot): pair backticks the way markdown does, and bound the scan
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 `<workspace_resource>` 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` <tag> 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) <noreply@anthropic.com>
1 parent 66a9500 commit bad05fe

2 files changed

Lines changed: 87 additions & 25 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,38 @@ describe('sanitizeChatDisplayContent', () => {
4848
expect(sanitizeChatDisplayContent(content)).toBe(content)
4949
})
5050

51+
it('leaves a neighbouring code span alone', () => {
52+
// Only a backtick DIRECTLY against the tag is the tag's wrapping. Allowing
53+
// whitespace between let the pattern reach past the tag and take the
54+
// delimiter off an unrelated span, breaking its pair.
55+
const content =
56+
'Open `config.json` <workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource> then run `bun test`.'
57+
58+
expect(sanitizeChatDisplayContent(content)).toBe(content)
59+
})
60+
61+
it('does not break the closing fence of a code block containing a tag', () => {
62+
// Whitespace matching used to cross the newline and consume one of the three
63+
// closing backticks, so the block never closed and the rest of the message
64+
// rendered as code.
65+
const content =
66+
'Example:\n```md\n<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource>\n```\nDone.'
67+
68+
expect(sanitizeChatDisplayContent(content)).toBe(content)
69+
})
70+
71+
it('stays linear on a message that repeats the tag name without ever closing it', () => {
72+
// A lazy scan allowed to cross an opener restarts from every opener, which
73+
// is quadratic — 154ms for this input before the bound, on the main thread,
74+
// for every streamed chunk. Generous ceiling so the test pins the complexity
75+
// rather than the machine.
76+
const content = 'The <workspace_resource> tag is used here. '.repeat(4000)
77+
78+
const startedAt = performance.now()
79+
sanitizeChatDisplayContent(content)
80+
expect(performance.now() - startedAt).toBeLessThan(50)
81+
})
82+
5183
it('still unwraps a real tag that carries a stray backtick on one side only', () => {
5284
// The case the unpaired strip is actually for: the model backticked the
5385
// opener but not the closer (or vice versa), which would block the chip.
Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,65 @@
11
const HIDDEN_INLINE_REFERENCE_PATTERN =
22
/`[^`\n]*(?:internal\/tool-results\/|internal\/blocktips\/|components\/integrations\/[^`\n]*README)[^`\n]*`/g
3-
const WORKSPACE_RESOURCE_CODE_SPAN_PATTERN =
4-
/`([^`\n]*<workspace_resource>[^`]*?<\/workspace_resource>[^`\n]*)`/g
53

64
/**
7-
* A stray backtick on ONE side of a complete resource tag, which would still
8-
* stop the chip rendering after the balanced case above is unwrapped.
5+
* A complete workspace-resource tag, with any backtick sitting directly against
6+
* either end. Replacing with the tag alone drops those backticks, which is what
7+
* lets the chip render when the model wrapped the tag in a code span.
98
*
10-
* All three patterns forbid a backtick between opener and closer, and that is
11-
* the load-bearing part. It separates a real tag from prose MENTIONING the tag
12-
* name: a payload is JSON and carries no backticks, whereas a message explaining
13-
* the syntax writes an opener and a closer as two separately backticked spans
14-
* with prose between them. Without the restriction, that prose reads as one
15-
* wrapped tag and its outer backticks are stripped — or a lone mention loses its
16-
* opening backtick — leaving an unpaired one that opens a code span running to
17-
* the next backtick, inverting every code span in the rest of the message.
9+
* Three constraints, each load-bearing:
1810
*
19-
* The trade: a resource whose title or path itself contains a backtick will not
20-
* have wrapping backticks stripped, so it renders as text rather than a chip.
21-
* That is the better failure. Prose explaining the tag is common and its damage
22-
* is message-wide; a backtick inside a filename is rare and costs one chip.
11+
* - **The closer must be present.** Prose MENTIONING the tag name writes
12+
* `<workspace_resource>` with no closer at all. Stripping a mention's opening
13+
* backtick leaves the closing one unpaired, and it opens a code span that runs
14+
* to the next backtick — inverting every code span in the rest of the message.
15+
* - **No backtick between opener and closer.** A payload is JSON and carries
16+
* none, while a message explaining the syntax writes the opener and the closer
17+
* as two separately backticked spans. Without this, that prose reads as one
18+
* wrapped tag and loses its outer pair.
19+
* - **No `<workspace_resource>` between opener and closer**, via the negative
20+
* lookahead. This is a cost bound, not a correctness rule: a lazy scan that may
21+
* cross an opener restarts from every opener, so a message repeating the tag
22+
* name is quadratic. Measured at 168KB of such prose: 154ms per call without
23+
* it, 0.34ms with — and this runs on the main thread for every streamed chunk.
24+
*
25+
* Backticks must be DIRECTLY adjacent. Allowing whitespace between let the
26+
* pattern reach past the tag and take the delimiter off a neighbouring code
27+
* span — `` Open `config.json` <tag> `` lost a backtick that way, and `\s*`
28+
* crossing a newline broke the closing fence of a code block containing a tag.
29+
*
30+
* Accepted trade: a resource whose title or path itself contains a backtick is
31+
* not matched, so it renders as text rather than a chip. That costs one chip and
32+
* is rare; the failure it replaces corrupts a whole message and is common.
2333
*/
24-
const WORKSPACE_RESOURCE_LEADING_BACKTICK =
25-
/`(\s*<workspace_resource>[^`]*?<\/workspace_resource>)/g
26-
const WORKSPACE_RESOURCE_TRAILING_BACKTICK =
27-
/(<workspace_resource>[^`]*?<\/workspace_resource>\s*)`/g
34+
const WORKSPACE_RESOURCE_TAG_WITH_BACKTICKS =
35+
/`?(<workspace_resource>(?:(?!<workspace_resource>)[^`])*?<\/workspace_resource>)`?/g
36+
37+
/**
38+
* An inline code span, paired the way markdown pairs one: opening backtick to
39+
* the next backtick on the same line.
40+
*
41+
* Pairing matters. A pattern that instead looked for "a backtick, then a tag,
42+
* then a backtick" would happily start at one span's delimiter and end at a
43+
* different span's, unwrapping the text between two unrelated spans — which is
44+
* how `` Open `config.json` <tag> then run `bun test` `` lost a backtick.
45+
*/
46+
const CODE_SPAN_PATTERN = /`([^`\n]*)`/g
47+
48+
/** Non-global so {@link RegExp.test} has no `lastIndex` to carry between calls. */
49+
const COMPLETE_WORKSPACE_RESOURCE_TAG =
50+
/<workspace_resource>(?:(?!<workspace_resource>)[^`])*?<\/workspace_resource>/
2851

2952
export function sanitizeChatDisplayContent(content: string): string {
30-
return content
31-
.replace(WORKSPACE_RESOURCE_CODE_SPAN_PATTERN, '$1')
32-
.replace(HIDDEN_INLINE_REFERENCE_PATTERN, '')
33-
.replace(WORKSPACE_RESOURCE_LEADING_BACKTICK, '$1')
34-
.replace(WORKSPACE_RESOURCE_TRAILING_BACKTICK, '$1')
53+
return (
54+
content
55+
// A tag inside a code span: drop the delimiters, keep the content. The
56+
// parser extracts the tag either way, so leaving them would strand a pair
57+
// of backticks around a hole once the chip is lifted out.
58+
.replace(CODE_SPAN_PATTERN, (span, inner: string) =>
59+
COMPLETE_WORKSPACE_RESOURCE_TAG.test(inner) ? inner : span
60+
)
61+
.replace(HIDDEN_INLINE_REFERENCE_PATTERN, '')
62+
// A leftover backtick pressed against a tag, with no partner to pair with.
63+
.replace(WORKSPACE_RESOURCE_TAG_WITH_BACKTICKS, '$1')
64+
)
3565
}

0 commit comments

Comments
 (0)