diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 10:55:51 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 10:55:51 +0900 |
| commit | 38db3827870960f466be89afbc49f91238d46144 (patch) | |
| tree | 24cb1b896dfadc31e72552dbe67f00530881242e /src/features/tabs | |
| parent | 17ce47987e673b6618454d033885b17b2a01912e (diff) | |
| download | dispatch-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.ts | 32 | ||||
| -rw-r--r-- | src/features/tabs/tabs.test.ts | 1 | ||||
| -rw-r--r-- | src/features/tabs/tabs.ts | 12 | ||||
| -rw-r--r-- | src/features/tabs/ui.test.ts | 10 |
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: { |
