From 2a7708bd492a5a78794c76ee43355cabe786943e Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 22 Jun 2026 16:09:56 +0900 Subject: feat: double-click tab to rename (PUT /conversations/:id/title) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Double-click a tab's title to enter inline edit mode. Enter or click away (blur) saves; Escape cancels. The rename is optimistic — the local tab updates immediately and PUT /title fires in the background. 683 tests green. --- src/app/App.svelte | 1 + src/app/store.svelte.ts | 13 +++++++++ src/features/tabs/ui/TabBar.svelte | 59 +++++++++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/app/App.svelte b/src/app/App.svelte index ae09bd5..9225cc7 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -309,6 +309,7 @@ onSelect={(id) => store.selectTab(id)} onClose={(id) => store.closeTab(id)} onNewDraft={() => store.newDraft()} + onRename={(id, title) => store.renameTab(id, title)} /> { + // Best-effort — the local tab is already renamed. + }); + }, + invoke(surfaceId: string, actionId: string, payload?: unknown): void { const result = protocolInvoke( protocol, diff --git a/src/features/tabs/ui/TabBar.svelte b/src/features/tabs/ui/TabBar.svelte index 9d224b9..f783412 100644 --- a/src/features/tabs/ui/TabBar.svelte +++ b/src/features/tabs/ui/TabBar.svelte @@ -9,6 +9,7 @@ onSelect, onClose, onNewDraft, + onRename, }: { tabs: readonly Tab[]; activeConversationId: string | null; @@ -17,6 +18,7 @@ onSelect: (conversationId: string) => void; onClose: (conversationId: string) => void; onNewDraft: () => void; + onRename?: (conversationId: string, title: string) => void; } = $props(); // The new-chat button is `position: sticky; right: 0`. It floats over the tabs @@ -65,6 +67,31 @@ ro?.disconnect(); }; }); + // Inline rename: double-click a tab's title to edit, Enter/blur to save. + let editingId = $state(null); + let editValue = $state(""); + let editEl = $state(); + + function startRename(tab: Tab): void { + if (onRename === undefined) return; + editingId = tab.conversationId; + editValue = tab.title; + // Focus the input after it renders. + queueMicrotask(() => editEl?.focus()); + } + + function commitRename(): void { + const id = editingId; + if (id !== null && onRename !== undefined) { + const trimmed = editValue.trim(); + if (trimmed.length > 0) onRename(id, trimmed); + } + editingId = null; + } + + function cancelRename(): void { + editingId = null; + }
@@ -86,7 +113,37 @@ > {handles.get(tab.conversationId) ?? tab.conversationId} - {tab.title} + {#if editingId === tab.conversationId} + e.stopPropagation()} + onkeydown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + commitRename(); + } else if (e.key === "Escape") { + e.preventDefault(); + cancelRename(); + } + }} + onblur={commitRename} + /> + {:else} + { + e.stopPropagation(); + startRename(tab); + }} + > + {tab.title} + + {/if} {#if statusFor?.(tab.conversationId) === "active"} {/if} -- cgit v1.2.3