Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
287ae8e
fix(copilot): render unclosed special tags as text on completed messages
j15z Jul 25, 2026
08c3d69
improvement(copilot): show text as soon as an unclosed tag cannot res…
j15z Jul 25, 2026
936bf03
fix(copilot): ignore tag syntax quoted inside a JSON tag body
j15z Jul 25, 2026
f1213c6
fix(copilot): keep prose a mispaired tag would swallow, and bail on d…
j15z Jul 25, 2026
e4ba2f6
fix(copilot): keep prose a tag wrapped instead of a JSON payload
j15z Jul 25, 2026
9492de4
refactor(copilot): resolve each special tag through four named outcomes
j15z Jul 25, 2026
ce1a898
test(copilot): pin the no-closing-tag case as lossless
j15z Jul 25, 2026
ea3a181
fix(copilot): stop the literal path flashing payloads and rescanning …
j15z Jul 25, 2026
6945d80
refactor(copilot): drop the JSON-prefix wrapper the split orphaned
j15z Jul 27, 2026
e7adb0e
refactor(copilot): drop the nested-marker scan the JSON rule already …
j15z Jul 27, 2026
c6d6368
fix(copilot): stop the rescan deleting quoted text, and bound its cost
j15z Jul 27, 2026
600425b
test(copilot): assert what a reader sees, and pin the unclosed-thinki…
j15z Jul 27, 2026
c469644
docs(copilot): name the scan window's blind spot instead of glossing it
j15z Jul 27, 2026
6f9ae20
fix(copilot): make the indexOf cache correct for any call order
j15z Jul 28, 2026
09366ee
fix(copilot): resume at the window edge, not past the close, on a tru…
j15z Jul 28, 2026
fc03111
fix(copilot): only discard a body that actually parsed as JSON
j15z Jul 28, 2026
78f66c7
fix(copilot): apply the nesting rule to a prose body on the matched-p…
j15z Jul 28, 2026
018fb41
test(copilot): assert parser invariants over generated messages
j15z Jul 28, 2026
d48670c
fix(copilot): strip a stray null byte from the test file
j15z Jul 28, 2026
75ec1f4
refactor(copilot): split resolveTagAt into budget, classification, an…
j15z Jul 28, 2026
3064101
fix(copilot): stop backtick stripping from unbalancing a whole message
j15z Jul 28, 2026
3484875
fix(copilot): apply the same backtick guard to the balanced unwrap
j15z Jul 28, 2026
be6d47c
fix(copilot): pair backticks the way markdown does, and bound the scan
j15z Jul 28, 2026
3996303
fix(copilot): address the confirmed review findings
j15z Jul 28, 2026
a3ba7a6
refactor(copilot): quality pass over the parser and its tests
j15z Jul 28, 2026
a6f7f7d
docs(copilot): reattach an orphaned docstring and cut comment noise
j15z Jul 28, 2026
86fe480
fix(copilot): sanitize backticks in one pass so a flush code span sur…
j15z Jul 28, 2026
2a4c0f4
fix(copilot): rewind the scan-window resume so a straddling opener st…
j15z Jul 29, 2026
1943cfe
fix(copilot): trust the raw body for markers once it is known not to …
j15z Jul 29, 2026
5066af1
refactor(copilot): share the scaling-ratio harness between the two pe…
j15z Jul 29, 2026
2972209
docs(copilot): correct a resume comment the rewind fix left inaccurate
j15z Jul 29, 2026
540667a
test(copilot): pin the blanking rule with a body staging cannot recover
j15z Jul 29, 2026
47e1f2f
test(copilot): cover every tag in the property invariants, and pin th…
j15z Jul 29, 2026
6fbd399
docs(copilot): state the actual bound on the thinking nesting rule
j15z Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -19,4 +20,96 @@ 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 `<workspace_resource>` 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 `<workspace_resource>` for files.\n\nThe `<workspace_resource>` chip needs an `id`.'
const backticks = (text: string) => (text.match(/`/g) || []).length

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 `<workspace_resource>` then close with `</workspace_resource>` at the end.'

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` <workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource> then run `bun test`.'

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`<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource> ok'
const after =
'Open <workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource>`config.json` ok'
const both =
'Open `a`<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource>`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
// rendered as code.
const content =
'Example:\n```md\n<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource>\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.
//
// 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', () => {
// 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 =
'`<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource> done'
const trailing =
'<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource>` done'

expect(sanitizeChatDisplayContent(leading)).toBe(
'<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource> done'
)
expect(sanitizeChatDisplayContent(trailing)).toBe(
'<workspace_resource>{"type":"file","path":"a.md","title":"a"}</workspace_resource> done'
)
})
})
Original file line number Diff line number Diff line change
@@ -1,12 +1,63 @@
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>[\s\S]*?<\/workspace_resource>[^`\n]*)`/g

/**
* A complete workspace-resource tag: opener, payload, closer.
*
* Two constraints on the payload, both load-bearing:
*
* - **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 COMPLETE_TAG_SOURCE =
'<workspace_resource>(?:(?!<workspace_resource>)[^`])*?<\\/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)

/**
* 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`<tag> `` keeps its pair without a special case.
*
* 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
* `` <tag>`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_OR_FLANKED_TAG = new RegExp(
`\`[^\`\\n]*\`|\`?(${COMPLETE_TAG_SOURCE})(?:\`(?![^\`\\n]*\`))?`,
'g'
)

export function sanitizeChatDisplayContent(content: string): string {
return content
.replace(WORKSPACE_RESOURCE_CODE_SPAN_PATTERN, '$1')
.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, '')
.replace(/`(\s*<workspace_resource>)/g, '$1')
.replace(/(<\/workspace_resource>\s*)`/g, '$1')
}
Original file line number Diff line number Diff line change
@@ -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 <workspace_resource> 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
}
Loading
Loading