summaryrefslogtreecommitdiffhomepage
path: root/src/features
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 16:54:20 +0900
committerAdam Malczewski <[email protected]>2026-06-27 16:54:20 +0900
commit3b3b0ebb340297795e075e46cdd9ced00b34bf60 (patch)
treeb78ecc2cc555a1dac89993a32371449c53f3375d /src/features
parenteb1fbf136dfce5099afbf1a2f1411a672656a59a (diff)
downloaddispatch-web-3b3b0ebb340297795e075e46cdd9ced00b34bf60.tar.gz
dispatch-web-3b3b0ebb340297795e075e46cdd9ced00b34bf60.zip
fix(sidebar-tabs): use text-selection highlight as copy indicator instead of swapping text
Diffstat (limited to 'src/features')
-rw-r--r--src/features/tabs/ui.test.ts11
-rw-r--r--src/features/tabs/ui/TabList.svelte40
2 files changed, 18 insertions, 33 deletions
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<string | null>(null);
- let copyTimer: ReturnType<typeof setTimeout> | 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<void> {
+ 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 @@
>
<button
type="button"
- class="shrink-0 rounded px-1 py-0.5 font-mono text-[10px] leading-none transition-colors {copiedId ===
- tab.conversationId
- ? "bg-success text-success-content"
- : "bg-base-300 text-base-content/60 hover:bg-primary hover:text-primary-content"}"
+ class="shrink-0 rounded bg-base-300 px-1 py-0.5 font-mono text-[10px] leading-none text-base-content/60 transition-colors hover:bg-primary hover:text-primary-content"
data-copy-id={tab.conversationId}
- title={copiedId === tab.conversationId ? "Copied!" : "Click to copy conversation id"}
+ title="Click to copy conversation id"
aria-label={`Copy conversation id ${tab.conversationId}`}
onclick={(e) => {
e.stopPropagation();
void copyId(tab.conversationId, e.currentTarget);
}}
>
- {copiedId === tab.conversationId
- ? "Copied!"
- : (handles.get(tab.conversationId) ?? tab.conversationId)}
+ {handles.get(tab.conversationId) ?? tab.conversationId}
</button>
{#if editingId === tab.conversationId}
<input