From dd98d4298e85929e87e74e143424a976a32b6f0f Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sat, 7 Nov 2026 23:50:52 -0800 Subject: [PATCH 1/2] fix(chat): keep thread links visible across paragraph boundaries Do not let an unmatched inline-code delimiter consume thread links in later paragraphs. Preserve multiline code spans within a paragraph and fenced code. Extend the existing link test with LF/CRLF and whitespace-only blank lines. The four isolated pattern regressions fail before this change and pass after; four inline/fenced-code controls also pass. Repository check:changed and integrated client validation remain required; dependencies could not be installed in this network-isolated audit environment. --- packages/shared/src/threadLinks.test.ts | 25 +++++++++++++++++++++++++ packages/shared/src/threadLinks.ts | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/threadLinks.test.ts b/packages/shared/src/threadLinks.test.ts index 5285214b8656d..9acc4a40e8901 100644 --- a/packages/shared/src/threadLinks.test.ts +++ b/packages/shared/src/threadLinks.test.ts @@ -150,6 +150,31 @@ describe("thread links", () => { "After [New](t3-thread://v2/local/t1)", ].join("\n"), ); + for (const literal of [ + "`multiline\n\ncode`", + "```md\n\n\n\n```", + "~~~md\n\n\n\n~~~", + ]) { + expect(threadLinkRefs(literal, local)).toEqual([]); + expect(relabelThreadLinks(literal, local, () => "New")).toBe(literal); + } + for (const newline of ["\n", "\r\n"]) { + for (const blank of ["", " \t"]) { + const paragraphs = [ + "An unmatched ` in one paragraph.", + blank, + "< t3-thread://v1/t1 >", + blank, + "Another ` in a later paragraph.", + ].join(newline); + const normalized = paragraphs.replace("< t3-thread://v1/t1 >", ""); + expect(trimThreadAutolinkPadding(paragraphs)).toBe(normalized); + expect(threadLinkRefs(normalized, local)).toEqual([ref("local", "t1")]); + expect(relabelThreadLinks(normalized, local, () => "New")).toBe( + paragraphs.replace("< t3-thread://v1/t1 >", "[New](t3-thread://v1/t1)"), + ); + } + } }); it("trims the whitespace a model writes inside a thread autolink's brackets, and nothing else", () => { diff --git a/packages/shared/src/threadLinks.ts b/packages/shared/src/threadLinks.ts index e3a5c5eb1ac03..2e50c961df6d4 100644 --- a/packages/shared/src/threadLinks.ts +++ b/packages/shared/src/threadLinks.ts @@ -11,7 +11,7 @@ const HOSTED_PREFIX = `${DEFAULT_HOSTED_APP_URL}/threads/`; const HOSTED_SEGMENT = "[A-Za-z0-9_%-]+"; const HOSTED_LINK = `${HOSTED_PREFIX.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}${HOSTED_SEGMENT}/${HOSTED_SEGMENT}`; // Code comes first in an alternation so a link written inside a code span or fence is skipped. -const CODE = String.raw`(?(\`{3,}|~{3,})[\s\S]*?(?:\2|(?![\s\S])))|(?(\`+)[\s\S]*?\4)`; +const CODE = String.raw`(?(\`{3,}|~{3,})[\s\S]*?(?:\2|(?![\s\S])))|(?(\`+)(?:(?!\r?\n[ \t]*\r?\n)[\s\S])*?\4)`; const THREAD_LINK_OUTSIDE_CODE = new RegExp( String.raw`${CODE}|\[(?