summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/features/tabs/ui.test.ts32
-rw-r--r--src/features/tabs/ui/TabList.svelte62
2 files changed, 88 insertions, 6 deletions
diff --git a/src/features/tabs/ui.test.ts b/src/features/tabs/ui.test.ts
index ff342d0..57f9bc1 100644
--- a/src/features/tabs/ui.test.ts
+++ b/src/features/tabs/ui.test.ts
@@ -1,4 +1,4 @@
-import { render, screen } from "@testing-library/svelte";
+import { fireEvent, render, screen } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import type { Tab } from "./tabs";
@@ -229,4 +229,34 @@ describe("TabList", () => {
expect(onRename).toHaveBeenCalledTimes(1);
expect(onRename).toHaveBeenCalledWith("c1", "Renamed");
});
+
+ it("copies the full conversation id to the clipboard and shows 'Copied!' when the ID badge is clicked", async () => {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ Object.defineProperty(navigator, "clipboard", {
+ value: { writeText },
+ configurable: true,
+ });
+
+ const onSelect = vi.fn();
+
+ render(TabList, {
+ props: {
+ tabs: sampleTabs,
+ activeConversationId: "c1",
+ onSelect,
+ onClose: vi.fn(),
+ onNewDraft: vi.fn(),
+ },
+ });
+
+ const idBadge = screen.getByRole("button", { name: "Copy conversation id c1" });
+ await fireEvent.click(idBadge);
+
+ expect(writeText).toHaveBeenCalledTimes(1);
+ expect(writeText).toHaveBeenCalledWith("c1");
+ // The badge briefly shows a "Copied!" confirmation.
+ expect(idBadge).toHaveTextContent("Copied!");
+ // 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 efef4fa..40f95cd 100644
--- a/src/features/tabs/ui/TabList.svelte
+++ b/src/features/tabs/ui/TabList.svelte
@@ -55,6 +55,46 @@
function cancelRename(): void {
editingId = null;
}
+
+ // 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;
+
+ async function copyId(conversationId: string, el: HTMLElement): Promise<void> {
+ const clipboard = navigator.clipboard;
+ if (clipboard === undefined) {
+ selectText(el);
+ flashCopied(conversationId);
+ return;
+ }
+ try {
+ await clipboard.writeText(conversationId);
+ flashCopied(conversationId);
+ } catch {
+ selectText(el);
+ flashCopied(conversationId);
+ }
+ }
+
+ 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);
+ const selection = window.getSelection();
+ selection?.removeAllRanges();
+ selection?.addRange(range);
+ }
</script>
<div class="flex flex-col gap-2">
@@ -75,12 +115,24 @@
if (e.key === "Enter") onSelect(tab.conversationId);
}}
>
- <span
- class="shrink-0 rounded bg-base-300 px-1 py-0.5 font-mono text-[10px] leading-none text-base-content/60"
- title="Tab ID"
+ <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"}"
+ data-copy-id={tab.conversationId}
+ title={copiedId === tab.conversationId ? "Copied!" : "Click to copy conversation id"}
+ aria-label={`Copy conversation id ${tab.conversationId}`}
+ onclick={(e) => {
+ e.stopPropagation();
+ void copyId(tab.conversationId, e.currentTarget);
+ }}
>
- {handles.get(tab.conversationId) ?? tab.conversationId}
- </span>
+ {copiedId === tab.conversationId
+ ? "Copied!"
+ : (handles.get(tab.conversationId) ?? tab.conversationId)}
+ </button>
{#if editingId === tab.conversationId}
<input
bind:this={editEl}