Skip to content

Commit 6ba39c3

Browse files
committed
fix(chat): scope delimiter pairing to paragraphs
1 parent 2d5b329 commit 6ba39c3

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@ describe('sanitizeChatDisplayContent', () => {
3535
expect(sanitizeChatDisplayContent(`${prefix}\`${tag}\``)).toBe(`${prefix}${tag}`)
3636
})
3737

38+
it.each(['\n\n', '\r\n\r\n', '\n \t\n'])(
39+
'does not pair prose runs across paragraph break %j',
40+
(separator) => {
41+
const tag = '<source>{"url":"https://example.com"}</source>'
42+
const before = `Use \`\` as a delimiter.${separator}`
43+
const after = `${separator}Another \`\` marker.`
44+
45+
expect(sanitizeChatDisplayContent(`${before}\`${tag}\`${after}`)).toBe(
46+
`${before}${tag}${after}`
47+
)
48+
}
49+
)
50+
51+
it('preserves matched multi-backtick spans across a soft line break', () => {
52+
const content = '``Literal\n`<source>{"url":"https://example.com"}</source>`\nexample``'
53+
54+
expect(sanitizeChatDisplayContent(content)).toBe(content)
55+
})
56+
57+
it('does not treat blank lines inside chip JSON as paragraph breaks', () => {
58+
const tag = '<source>{\n\n"url":"https://example.com",\n\n"title":"Use `code`"\n}</source>'
59+
60+
expect(sanitizeChatDisplayContent(`\`${tag}\``)).toBe(tag)
61+
})
62+
3863
it('preserves fences closed by a longer run and unwraps citations after them', () => {
3964
const tag = '<source>{"url":"https://example.com"}</source>'
4065
const block = `\`\`\`json\n\`${tag}\`\n\`\`\`\`\n`

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const JSON_STRING_SOURCE = '"(?:\\\\(?:["\\\\/bfnrt]|u[0-9a-fA-F]{4})|[^"\\\\\\r
88
const COMPLETE_TAG_SOURCE = `<(?<chipTag>workspace_resource|source)>\\s*\\{(?:${JSON_STRING_SOURCE}|[^"\`<\\\\])*?\\}\\s*</\\k<chipTag>>`
99

1010
const INLINE_CHIP_OR_DELIMITER = new RegExp(`${COMPLETE_TAG_SOURCE}|\`+|\\n`, 'g')
11+
const CHIP_OR_PARAGRAPH_BREAK = new RegExp(`${COMPLETE_TAG_SOURCE}|\\n[\\t \\r]*\\n`, 'g')
1112

1213
interface OpenCodeSpan {
1314
index: number
@@ -16,7 +17,7 @@ interface OpenCodeSpan {
1617
}
1718

1819
/** Only matched multi-backtick runs are code; an unmatched run remains ordinary prose. */
19-
function unwrapInlineChips(content: string): string {
20+
function unwrapInlineParagraph(content: string): string {
2021
const remainingRuns = new Map<number, number>()
2122
for (const [value] of content.matchAll(INLINE_CHIP_OR_DELIMITER)) {
2223
if (value.startsWith('`') && value.length > 1) {
@@ -77,6 +78,20 @@ function unwrapInlineChips(content: string): string {
7778
return parts.join('')
7879
}
7980

81+
/** Paragraph breaks end inline spans, but blank lines inside chip JSON belong to the payload. */
82+
function unwrapInlineChips(content: string): string {
83+
const parts: string[] = []
84+
let cursor = 0
85+
86+
for (const match of content.matchAll(CHIP_OR_PARAGRAPH_BREAK)) {
87+
if (!match[0].startsWith('\n')) continue
88+
parts.push(unwrapInlineParagraph(content.slice(cursor, match.index)), match[0])
89+
cursor = match.index + match[0].length
90+
}
91+
parts.push(unwrapInlineParagraph(content.slice(cursor)))
92+
return parts.join('')
93+
}
94+
8095
/** Fenced blocks are literal, including unclosed streaming fences and longer closing runs. */
8196
export function sanitizeChatDisplayContent(content: string): string {
8297
const parts: string[] = []

0 commit comments

Comments
 (0)