summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 16:08:10 +0900
committerAdam Malczewski <[email protected]>2026-06-27 16:08:10 +0900
commit6bd8f4718b44d06d116d02d9538214c8450b1c8d (patch)
treed709dc077be66cbf22108e492472e27cf731440d
parent0e0601817712033b3247695646acd22d6496330a (diff)
downloaddispatch-web-6bd8f4718b44d06d116d02d9538214c8450b1c8d.tar.gz
dispatch-web-6bd8f4718b44d06d116d02d9538214c8450b1c8d.zip
feat(sidebar-tabs): add active tab title to top bar (New Tab for unstarted chats)
-rw-r--r--src/app/App.svelte27
-rw-r--r--src/app/App.test.ts40
2 files changed, 64 insertions, 3 deletions
diff --git a/src/app/App.svelte b/src/app/App.svelte
index 9a17999..1bb5689 100644
--- a/src/app/App.svelte
+++ b/src/app/App.svelte
@@ -216,6 +216,18 @@
return parseTodoPayload(field.payload);
});
+ // Top-bar title: the active tab's title, or "New Tab" when no tab is active
+ // (a fresh, unstarted draft — the conversation hasn't been sent yet, so no
+ // tab exists). Pure-derived from the (workspace-filtered) tab set + the
+ // active id; reflects whichever tab is selected in the sidebar's Tabs view.
+ const NEW_TAB_TITLE = "New Tab";
+ const topBarTitle = $derived.by(() => {
+ const id = store.activeConversationId;
+ if (id === null) return NEW_TAB_TITLE;
+ const tab = store.tabs.find((t) => t.conversationId === id);
+ return tab?.title ?? NEW_TAB_TITLE;
+ });
+
// Conversation/tab switch → snap to the bottom of the new transcript.
$effect(() => {
void store.activeConversationId;
@@ -421,9 +433,18 @@
(below), so opening it shrinks this ENTIRE column. -->
<div class="flex min-w-0 flex-1 flex-col overflow-hidden pt-[5px]">
<!-- Slim header: the tab bar moved into the sidebar (the "Tabs" view), so
- the top row now holds only the build version + the sidebar toggle,
- right-aligned. -->
- <div class="flex items-center justify-end px-1">
+ the top row now shows the active tab's title on the left (or "New Tab"
+ for an unstarted draft), with the build version + sidebar toggle on
+ the right. -->
+ <div class="flex items-center justify-between gap-2 px-2">
+ <span
+ class="min-w-0 flex-1 shrink truncate text-sm font-medium opacity-70"
+ data-testid="top-bar-title"
+ title={topBarTitle}
+ aria-label="Active conversation title"
+ >
+ {topBarTitle}
+ </span>
<span
class="shrink-0 select-none px-1 font-mono text-[10px] leading-none text-base-content/30"
title="Build version (git short hash)"
diff --git a/src/app/App.test.ts b/src/app/App.test.ts
index 7c0a851..3d6e70c 100644
--- a/src/app/App.test.ts
+++ b/src/app/App.test.ts
@@ -136,6 +136,46 @@ describe("App component interaction tests", () => {
store.dispose();
});
+ it("shows 'New Tab' in the top bar for an unstarted draft (no active tab)", () => {
+ const ws = fakeSocket();
+ const store = createAppStore({
+ socketFactory: () => ws,
+ fetchImpl: fakeFetchImpl(),
+ localStorage: createFakeStorage(),
+ });
+ ws.resolveOpen();
+
+ render(App, { props: { store } });
+
+ expect(store.activeConversationId).toBeNull();
+ expect(screen.getByTestId("top-bar-title")).toHaveTextContent("New Tab");
+
+ store.dispose();
+ });
+
+ it("shows the active tab's title in the top bar once a tab is started", async () => {
+ const ws = fakeSocket();
+ const store = createAppStore({
+ socketFactory: () => ws,
+ fetchImpl: fakeFetchImpl(),
+ localStorage: createFakeStorage(),
+ });
+ ws.resolveOpen();
+
+ render(App, { props: { store } });
+
+ // Promote draft → tab: the tab's title is derived from the first message.
+ store.send("Refactor the auth module");
+ expect(store.activeConversationId).not.toBeNull();
+
+ // The top-bar title updates reactively; findByTestId awaits the flush.
+ expect(await screen.findByTestId("top-bar-title")).toHaveTextContent(
+ "Refactor the auth module",
+ );
+
+ store.dispose();
+ });
+
it("auto-subscribes to every catalog entry on render (no buttons to click)", () => {
const ws = fakeSocket();
const store = createAppStore({