From ef9a5f9889582e08467b01cead41aaafd70d220a Mon Sep 17 00:00:00 2001 From: harjoth Date: Fri, 10 Jul 2026 18:16:05 -0700 Subject: [PATCH] fix(desktop): preserve matched URL parentheses (cherry picked from commit 6e126d8575859f9ca7a6d20240e2e2ba61c74b7b) --- .../assistant-ui/markdown-text.test.ts | 18 ++++++++++ apps/desktop/src/lib/markdown-preprocess.ts | 35 ++++++++++--------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.test.ts b/apps/desktop/src/components/assistant-ui/markdown-text.test.ts index 5914bb7a09..413a2e2459 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +++ b/apps/desktop/src/components/assistant-ui/markdown-text.test.ts @@ -134,6 +134,24 @@ describe('preprocessMarkdown', () => { expect(output).toContain('') }) + it('preserves a trailing balanced pair after an earlier stray closing paren', () => { + const output = preprocessMarkdown('See https://example.com/a)(b)') + + expect(output).toContain('') + }) + + it('strips three wrapper closing parens from raw-url autolinks', () => { + const output = preprocessMarkdown('(((https://example.com/page)))') + + expect(output).toBe('((()))') + }) + + it('preserves an unmatched opening paren at the end of a raw URL', () => { + const output = preprocessMarkdown('https://example.com/foo(') + + expect(output).toContain('') + }) + it('strips only the wrapper closing paren around balanced-paren URLs', () => { const output = preprocessMarkdown('(https://example.com/wiki/Foo_(bar))') diff --git a/apps/desktop/src/lib/markdown-preprocess.ts b/apps/desktop/src/lib/markdown-preprocess.ts index eb606f9c29..59ee564702 100644 --- a/apps/desktop/src/lib/markdown-preprocess.ts +++ b/apps/desktop/src/lib/markdown-preprocess.ts @@ -211,29 +211,32 @@ function isUrlOnlyBlock(lines: string[]): boolean { // Strip only the *unbalanced* trailing `)` — the closing paren of a prose // wrapper like `(https://example.com/page)`, which the bare-URL matcher // greedily captures into the href. Parens that belong to the URL itself -// (`…/wiki/Foo_(bar)`) stay intact: a trailing `)` is peeled only while the -// URL holds more `)` than `(`. Count once, then trim from the end — the loop -// only ever removes trailing closers, so `opens` never changes. +// (`…/wiki/Foo_(bar)`) stay intact. Track normal left-to-right pairing and +// peel only a suffix made entirely of unmatched closing parens; this avoids +// sacrificing a valid trailing pair when an earlier stray `)` exists. function trimUnbalancedTrailingParens(url: string): string { - let opens = 0 - let closes = 0 + let depth = 0 + let unmatchedSuffixStart: number | null = null + + for (let index = 0; index < url.length; index += 1) { + const char = url[index] - for (const char of url) { if (char === '(') { - opens += 1 + depth += 1 + unmatchedSuffixStart = null } else if (char === ')') { - closes += 1 + if (depth > 0) { + depth -= 1 + unmatchedSuffixStart = null + } else if (unmatchedSuffixStart === null) { + unmatchedSuffixStart = index + } + } else { + unmatchedSuffixStart = null } } - let end = url.length - - while (closes > opens && url[end - 1] === ')') { - closes -= 1 - end -= 1 - } - - return url.slice(0, end) + return unmatchedSuffixStart === null ? url : url.slice(0, unmatchedSuffixStart) } function autoLinkRawUrls(text: string): string {