summaryrefslogtreecommitdiffhomepage
path: root/src/features/tabs
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
committerAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
commit38db3827870960f466be89afbc49f91238d46144 (patch)
tree24cb1b896dfadc31e72552dbe67f00530881242e /src/features/tabs
parent17ce47987e673b6618454d033885b17b2a01912e (diff)
downloaddispatch-web-38db3827870960f466be89afbc49f91238d46144.tar.gz
dispatch-web-38db3827870960f466be89afbc49f91238d46144.zip
feat: workspaces shell + cwd-lsp rename + mcp/settings/system-prompt features + app wiring
- workspaces: URL-driven conversation grouping (home listing at /, routing, store, http adapter, WorkspaceCard) wired into the App.svelte shell - rename features/workspace -> features/cwd-lsp (the cwd/lsp status feature) - new features: mcp (status view), settings (chat-limit field), system-prompt (prompt builder), all rendered via the generic surface host - chat: store + ChatView updates - tabs: tabs-store updates - app wiring: ErrorModal (full-screen error surface), app/App.svelte + store.svelte This commit makes HEAD typecheck clean for the first time: the prior HEAD (c95cc77) imported features/settings from app/App.svelte but never committed the feature, so only the full working tree was green.
Diffstat (limited to 'src/features/tabs')
-rw-r--r--src/features/tabs/tabs-store.test.ts32
-rw-r--r--src/features/tabs/tabs.test.ts1
-rw-r--r--src/features/tabs/tabs.ts12
-rw-r--r--src/features/tabs/ui.test.ts10
4 files changed, 33 insertions, 22 deletions
diff --git a/src/features/tabs/tabs-store.test.ts b/src/features/tabs/tabs-store.test.ts
index 81ec8ad..71a38dc 100644
--- a/src/features/tabs/tabs-store.test.ts
+++ b/src/features/tabs/tabs-store.test.ts
@@ -24,7 +24,7 @@ function createMemoryStorage(initial?: TabsState): TabsStorage & { data: TabsSta
describe("createTabsStore", () => {
it("loads persisted state on construct", () => {
const persisted: TabsState = {
- tabs: [{ conversationId: "c1", model: "m1", title: "T1" }],
+ tabs: [{ conversationId: "c1", model: "m1", title: "T1", workspaceId: "default" }],
activeConversationId: "c1",
};
const storage = createMemoryStorage(persisted);
@@ -48,11 +48,11 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "m1", title: "T1" });
+ store.createTab({ conversationId: "c1", model: "m1", title: "T1", workspaceId: "default" });
expect(storage.data?.tabs).toHaveLength(1);
expect(storage.data?.activeConversationId).toBe("c1");
- store.createTab({ conversationId: "c2", model: "m2", title: "T2" });
+ store.createTab({ conversationId: "c2", model: "m2", title: "T2", workspaceId: "default" });
expect(storage.data?.tabs).toHaveLength(2);
store.selectTab("c1");
@@ -76,11 +76,11 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "m1", title: "T1" });
+ store.createTab({ conversationId: "c1", model: "m1", title: "T1", workspaceId: "default" });
expect(store.tabs).toHaveLength(1);
expect(store.activeConversationId).toBe("c1");
- store.createTab({ conversationId: "c2", model: "m2", title: "T2" });
+ store.createTab({ conversationId: "c2", model: "m2", title: "T2", workspaceId: "default" });
expect(store.tabs).toHaveLength(2);
expect(store.activeConversationId).toBe("c2");
});
@@ -89,8 +89,8 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "m1", title: "T1" });
- store.createTab({ conversationId: "c2", model: "m2", title: "T2" });
+ store.createTab({ conversationId: "c1", model: "m1", title: "T1", workspaceId: "default" });
+ store.createTab({ conversationId: "c2", model: "m2", title: "T2", workspaceId: "default" });
store.selectTab("c1");
expect(store.activeConversationId).toBe("c1");
@@ -100,9 +100,9 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "m1", title: "T1" });
- store.createTab({ conversationId: "c2", model: "m2", title: "T2" });
- store.createTab({ conversationId: "c3", model: "m3", title: "T3" });
+ store.createTab({ conversationId: "c1", model: "m1", title: "T1", workspaceId: "default" });
+ store.createTab({ conversationId: "c2", model: "m2", title: "T2", workspaceId: "default" });
+ store.createTab({ conversationId: "c3", model: "m3", title: "T3", workspaceId: "default" });
store.selectTab("c2");
store.closeTab("c2");
@@ -114,7 +114,7 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "m1", title: "T1" });
+ store.createTab({ conversationId: "c1", model: "m1", title: "T1", workspaceId: "default" });
store.closeTab("c1");
expect(store.tabs).toHaveLength(0);
expect(store.activeConversationId).toBeNull();
@@ -124,8 +124,8 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "old", title: "T1" });
- store.createTab({ conversationId: "c2", model: "m2", title: "T2" });
+ store.createTab({ conversationId: "c1", model: "old", title: "T1", workspaceId: "default" });
+ store.createTab({ conversationId: "c2", model: "m2", title: "T2", workspaceId: "default" });
store.setModel("c1", "new-model");
expect(store.tabs[0]?.model).toBe("new-model");
@@ -136,7 +136,7 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "m1", title: "Old" });
+ store.createTab({ conversationId: "c1", model: "m1", title: "Old", workspaceId: "default" });
store.setTitle("c1", "New Title");
expect(store.tabs[0]?.title).toBe("New Title");
@@ -146,8 +146,8 @@ describe("createTabsStore", () => {
const storage = createMemoryStorage();
const store = createTabsStore(storage);
- store.createTab({ conversationId: "c1", model: "m1", title: "T1" });
- store.createTab({ conversationId: "c2", model: "m2", title: "T2" });
+ store.createTab({ conversationId: "c1", model: "m1", title: "T1", workspaceId: "default" });
+ store.createTab({ conversationId: "c2", model: "m2", title: "T2", workspaceId: "default" });
store.newDraft();
expect(store.tabs).toHaveLength(2);
diff --git a/src/features/tabs/tabs.test.ts b/src/features/tabs/tabs.test.ts
index 3c2a8c2..e3ca4fa 100644
--- a/src/features/tabs/tabs.test.ts
+++ b/src/features/tabs/tabs.test.ts
@@ -19,6 +19,7 @@ const tab = (conversationId: string, model = "default", title = "Chat"): Tab =>
conversationId,
model,
title,
+ workspaceId: "default",
});
describe("initialState", () => {
diff --git a/src/features/tabs/tabs.ts b/src/features/tabs/tabs.ts
index 3360d3f..53eda78 100644
--- a/src/features/tabs/tabs.ts
+++ b/src/features/tabs/tabs.ts
@@ -2,6 +2,8 @@ export interface Tab {
readonly conversationId: string;
readonly model: string;
readonly title: string;
+ /** The workspace this tab belongs to (the workspace's URL slug). */
+ readonly workspaceId: string;
}
export interface TabsState {
@@ -13,7 +15,15 @@ const DEFAULT_TITLE = "New chat";
const DEFAULT_MAX_TITLE_LENGTH = 40;
export function initialState(persisted?: TabsState): TabsState {
- if (persisted !== undefined) return persisted;
+ if (persisted !== undefined) {
+ // Migrate tabs persisted before workspaces: assign them to the "default"
+ // workspace (the fallback for conversations with no workspace).
+ const tabs = persisted.tabs.map((t) => {
+ const wid = (t as { workspaceId?: string }).workspaceId;
+ return { ...t, workspaceId: wid ?? "default" };
+ });
+ return { tabs, activeConversationId: persisted.activeConversationId };
+ }
return { tabs: [], activeConversationId: null };
}
diff --git a/src/features/tabs/ui.test.ts b/src/features/tabs/ui.test.ts
index 6cd66bd..a46b692 100644
--- a/src/features/tabs/ui.test.ts
+++ b/src/features/tabs/ui.test.ts
@@ -5,9 +5,9 @@ import type { Tab } from "./tabs";
import TabBar from "./ui/TabBar.svelte";
const sampleTabs: readonly Tab[] = [
- { conversationId: "c1", model: "openai/gpt-4", title: "First" },
- { conversationId: "c2", model: "anthropic/claude-3", title: "Second" },
- { conversationId: "c3", model: "google/gemini", title: "Third" },
+ { conversationId: "c1", model: "openai/gpt-4", title: "First", workspaceId: "default" },
+ { conversationId: "c2", model: "anthropic/claude-3", title: "Second", workspaceId: "default" },
+ { conversationId: "c3", model: "google/gemini", title: "Third", workspaceId: "default" },
];
describe("TabBar", () => {
@@ -178,8 +178,8 @@ describe("TabBar", () => {
it("renders a short-handle tab ID badge (shortest unique prefix) per tab", () => {
const tabs: readonly Tab[] = [
- { conversationId: "3f9a1b2c-1111", model: "m", title: "Alpha" },
- { conversationId: "7c2db4e5-2222", model: "m", title: "Beta" },
+ { conversationId: "3f9a1b2c-1111", model: "m", title: "Alpha", workspaceId: "default" },
+ { conversationId: "7c2db4e5-2222", model: "m", title: "Beta", workspaceId: "default" },
];
render(TabBar, {
props: {