From 62b4b2eeac19576fa99dca99ea062417e5f27353 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 7 Nov 2026 03:29:44 +0000 Subject: [PATCH 1/4] test(web): reproduce triple-click stopping at links (#32280) Triple-clicking plain text beside a favicon link or a file chip in a chat message selects only the text on one side of the link. Co-Authored-By: Claude Opus 5.5 --- .../components/ChatMarkdown.browser.test.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/web/src/components/ChatMarkdown.browser.test.tsx b/apps/web/src/components/ChatMarkdown.browser.test.tsx index 91d0abd6e341a..2106f35b57b39 100644 --- a/apps/web/src/components/ChatMarkdown.browser.test.tsx +++ b/apps/web/src/components/ChatMarkdown.browser.test.tsx @@ -431,6 +431,40 @@ describe("ChatMarkdown inline code selection", () => { await userEvent.dblClick(fenced, { position: { x: 4, y: 4 } }); expect(window.getSelection()?.toString()).toBe("fenced"); }); + + it("selects the whole line on triple-click beside a favicon link or a file chip", async () => { + setup(); + await using screen = await renderText( + "Before [docs](https://example.com/docs) after words\n\nBefore [App.tsx](/tmp/project/src/App.tsx) after words", + ); + const tripleClickWord = async (paragraph: HTMLElement, word: string) => { + const text = [...paragraph.childNodes].find((node) => node.textContent?.includes(word))!; + const range = document.createRange(); + range.setStart(text, text.textContent!.indexOf(word)); + range.setEnd(text, text.textContent!.indexOf(word) + word.length); + const wordBox = range.getBoundingClientRect(); + const paragraphBox = paragraph.getBoundingClientRect(); + window.getSelection()?.removeAllRanges(); + await userEvent.tripleClick(paragraph, { + position: { + x: wordBox.left - paragraphBox.left + wordBox.width / 2, + y: wordBox.top - paragraphBox.top + wordBox.height / 2, + }, + }); + return window.getSelection()?.toString().trim(); + }; + + const selections = []; + const lines = []; + for (const paragraph of screen.container.querySelectorAll("p")) { + lines.push(paragraph.textContent?.trim(), paragraph.textContent?.trim()); + selections.push( + await tripleClickWord(paragraph, "Before"), + await tripleClickWord(paragraph, "after"), + ); + } + expect(selections).toEqual(lines); + }); }); describe("ChatMarkdown Windows file links", () => { From db47e11d6c9844c1b0bf0b4252db814384bf7ed1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 7 Nov 2026 03:40:17 +0000 Subject: [PATCH 2/4] fix(web): triple-click selects the whole line around links and file chips Chromium ends a triple-click paragraph selection at any block-level or user-select: none box. Favicon links put a flex box and a select-none icon in the line, and markdown file chips are inline-flex with block icon and label, so triple-clicking text beside them selected only part of the line. The favicon wrapper is now a plain inline span holding a selectable inline-block icon at the same size and offset. Markdown file chips become inline-block with inline icon and label, clipping and ellipsizing on the chip instead of the label. ContextChip itself is unchanged for other surfaces. Co-Authored-By: Claude Opus 5.5 --- .../components/chat/MarkdownExternalLink.tsx | 8 +++----- apps/web/src/index.css | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/chat/MarkdownExternalLink.tsx b/apps/web/src/components/chat/MarkdownExternalLink.tsx index d462799564463..a5f4a9fd467cf 100644 --- a/apps/web/src/components/chat/MarkdownExternalLink.tsx +++ b/apps/web/src/components/chat/MarkdownExternalLink.tsx @@ -7,7 +7,8 @@ import { sourceControlClients } from "@t3tools/client-runtime/source-control-cli import { cn } from "../../lib/utils"; import { sourceControlIcon } from "../../sourceControlPresentation"; -const MARKDOWN_LINK_FAVICON_CLASS_NAME = "block size-full shrink-0 select-none"; +// Inline-level and selectable: triple-click selection stops at block-level and select-none boxes. +const MARKDOWN_LINK_FAVICON_CLASS_NAME = "inline-block size-[14px] [vertical-align:-0.125em]"; const failedFaviconHosts = new Set(); @@ -23,10 +24,7 @@ const MarkdownLinkFavicon = memo(function MarkdownLinkFavicon({ host }: { host: const brandIcon = sourceControlHostIcon(host); const faviconUrl = brandIcon ? null : faviconUrlForOrigin(`https://${host}`); return ( - + {brandIcon ? ( brandIcon ) : faviconUrl === null || failedHost === host || failedFaviconHosts.has(host) ? ( diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 3f3e1c742f4cf..9bf5536cf1488 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -2037,6 +2037,25 @@ code { text-decoration: none; } +/* Triple-click selection stops at block-level boxes, flex items included, so file chips in + prose stay inline-level down to the icon and label. */ +.chat-markdown-file-link { + display: inline-block; + overflow-x: clip; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-markdown-file-link > svg { + display: inline; + margin-inline-end: 0.2em; + vertical-align: -0.125em; +} + +.chat-markdown-file-link > [data-slot="context-chip-label"] { + display: inline; +} + /* File links and context chips own their color and focus ring. */ .chat-markdown a:not(.chat-markdown-file-link, :has([data-slot="context-chip-label"])) { color: var(--info-foreground); From 652dcb731022b97222014f81f12d3b6c5144651a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 17:01:18 +0000 Subject: [PATCH 3/4] test(web): require both paragraphs in the triple-click selection test Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/ChatMarkdown.browser.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatMarkdown.browser.test.tsx b/apps/web/src/components/ChatMarkdown.browser.test.tsx index 5ace6a9d58b36..965536558dc61 100644 --- a/apps/web/src/components/ChatMarkdown.browser.test.tsx +++ b/apps/web/src/components/ChatMarkdown.browser.test.tsx @@ -502,9 +502,11 @@ describe("ChatMarkdown inline code selection", () => { return window.getSelection()?.toString().trim(); }; + const paragraphs = screen.container.querySelectorAll("p"); + expect(paragraphs).toHaveLength(2); const selections = []; const lines = []; - for (const paragraph of screen.container.querySelectorAll("p")) { + for (const paragraph of paragraphs) { lines.push(paragraph.textContent?.trim(), paragraph.textContent?.trim()); selections.push( await tripleClickWord(paragraph, "Before"), From 7986be915763b2862f05712b80e1a4e095ec3ba8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 20:46:39 +0000 Subject: [PATCH 4/4] fix(web): keep the icon gap on app-bundle file chips AI-assisted; verified with the tests and tools listed above. --- apps/web/src/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 216eb1a0344c3..51c26de43c607 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -2046,7 +2046,7 @@ code { white-space: nowrap; } -.chat-markdown-file-link > svg { +.chat-markdown-file-link > :is(svg, img) { display: inline; margin-inline-end: 0.2em; vertical-align: -0.125em;