From 3b3b0ebb340297795e075e46cdd9ced00b34bf60 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 27 Jun 2026 16:54:20 +0900 Subject: fix(sidebar-tabs): use text-selection highlight as copy indicator instead of swapping text --- src/features/tabs/ui.test.ts | 11 +++++++--- src/features/tabs/ui/TabList.svelte | 40 ++++++++++--------------------------- 2 files changed, 18 insertions(+), 33 deletions(-) (limited to 'src/features') diff --git a/src/features/tabs/ui.test.ts b/src/features/tabs/ui.test.ts index 57f9bc1..9af4cd1 100644 --- a/src/features/tabs/ui.test.ts +++ b/src/features/tabs/ui.test.ts @@ -230,7 +230,7 @@ describe("TabList", () => { expect(onRename).toHaveBeenCalledWith("c1", "Renamed"); }); - it("copies the full conversation id to the clipboard and shows 'Copied!' when the ID badge is clicked", async () => { + it("copies the conversation id to the clipboard and highlights the badge text when clicked", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { value: { writeText }, @@ -254,8 +254,13 @@ describe("TabList", () => { expect(writeText).toHaveBeenCalledTimes(1); expect(writeText).toHaveBeenCalledWith("c1"); - // The badge briefly shows a "Copied!" confirmation. - expect(idBadge).toHaveTextContent("Copied!"); + // The badge text is NOT swapped (no width shift) — the highlight (selection) + // is the only indicator. + expect(idBadge).toHaveTextContent("c1"); + expect(idBadge).not.toHaveTextContent("Copied"); + // The badge text is selected (highlighted) as the copy indicator. + const selection = window.getSelection(); + expect(selection?.toString()).toBe("c1"); // Clicking the ID must NOT switch tabs (the badge stops propagation). expect(onSelect).not.toHaveBeenCalled(); }); diff --git a/src/features/tabs/ui/TabList.svelte b/src/features/tabs/ui/TabList.svelte index 40f95cd..031743c 100644 --- a/src/features/tabs/ui/TabList.svelte +++ b/src/features/tabs/ui/TabList.svelte @@ -58,36 +58,21 @@ // Click-to-copy the agent (conversation) id: clicking the ID badge copies the // FULL conversationId to the clipboard (the stable, useful id — the badge - // only shows a short prefix) and briefly highlights the badge as feedback. - // Clipboard is the edge effect; absent (insecure context) → select the badge - // text so the user can Ctrl+C manually. - let copiedId = $state(null); - let copyTimer: ReturnType | undefined; - + // only shows a short prefix) and highlights the badge text as feedback. The + // highlight (text selection) is the indicator — no text is swapped, so the + // badge keeps a stable width. If the clipboard write fails, the selection is + // already in place so the user can Ctrl+C the text manually. async function copyId(conversationId: string, el: HTMLElement): Promise { + selectText(el); const clipboard = navigator.clipboard; - if (clipboard === undefined) { - selectText(el); - flashCopied(conversationId); - return; - } + if (clipboard === undefined) return; try { await clipboard.writeText(conversationId); - flashCopied(conversationId); } catch { - selectText(el); - flashCopied(conversationId); + // Selection already lets the user copy manually. } } - function flashCopied(conversationId: string): void { - copiedId = conversationId; - clearTimeout(copyTimer); - copyTimer = setTimeout(() => { - copiedId = null; - }, 1200); - } - function selectText(el: HTMLElement): void { const range = document.createRange(); range.selectNodeContents(el); @@ -117,21 +102,16 @@ > {#if editingId === tab.conversationId}