From 4ba2673557c7cfd0bc31b03e2c35ab5e1efe60e7 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 21 May 2026 19:52:35 +0900 Subject: feat: tab system with per-tab agents, DB persistence, and DaisyUI tabs-lift UI - Add tabs, messages, and settings tables to SQLite database - Backend: refactor AgentManager to manage per-tab Agent instances via Map - Backend: WebSocket events tagged with tabId for multiplexing - Backend: tab CRUD routes (create, list, update, archive, messages) - Backend: persist user and assistant messages to DB during chat - Frontend: new tabStore replaces single chatStore with multi-tab reactive state - Frontend: TabBar component using DaisyUI tabs-lift style with status dots - Frontend: Settings sidebar panel for title generation model selection - Frontend: wire ChatPanel, ChatInput, Header to use tabStore - Fix HMR listener accumulation via wsClient.clearCallbacks() - Delete old single-chat chatStore (chat.svelte.ts) --- packages/api/src/routes/tabs.ts | 81 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 packages/api/src/routes/tabs.ts (limited to 'packages/api/src/routes') diff --git a/packages/api/src/routes/tabs.ts b/packages/api/src/routes/tabs.ts new file mode 100644 index 0000000..7fe05d9 --- /dev/null +++ b/packages/api/src/routes/tabs.ts @@ -0,0 +1,81 @@ +import { Hono } from "hono"; +import { + createTab, + getTab, + listOpenTabs, + updateTabTitle, + updateTabModel, + updateTabStatus, + archiveTab, + getMessagesForTab, + getSetting, + setSetting, +} from "@dispatch/core"; + +export const tabsRoutes = new Hono(); + +let getAgentManager: () => { stopTab(id: string): void; deleteTab(id: string): void } | null = () => null; + +export function setTabsAgentManager(getter: () => { stopTab(id: string): void; deleteTab(id: string): void } | null): void { + getAgentManager = getter; +} + +tabsRoutes.get("/", (c) => { + const tabs = listOpenTabs(); + return c.json({ tabs }); +}); + +tabsRoutes.post("/", async (c) => { + const body = await c.req.json<{ id?: string; title?: string }>(); + const id = body.id ?? crypto.randomUUID(); + const title = body.title ?? "New Tab"; + const tab = createTab(id, title); + return c.json(tab); +}); + +// Settings routes (must be before /:id to avoid conflict) +tabsRoutes.get("/settings/title-model", (c) => { + const keyId = getSetting("title_model_key_id"); + const modelId = getSetting("title_model_id"); + return c.json({ keyId, modelId }); +}); + +tabsRoutes.put("/settings/title-model", async (c) => { + const body = await c.req.json<{ keyId?: string; modelId?: string }>(); + if (body.keyId) setSetting("title_model_key_id", body.keyId); + if (body.modelId) setSetting("title_model_id", body.modelId); + return c.json({ success: true }); +}); + +tabsRoutes.get("/:id", (c) => { + const id = c.req.param("id"); + const tab = getTab(id); + if (!tab) return c.json({ error: "tab not found" }, 404); + return c.json(tab); +}); + +tabsRoutes.get("/:id/messages", (c) => { + const id = c.req.param("id"); + const messages = getMessagesForTab(id); + return c.json({ messages }); +}); + +tabsRoutes.patch("/:id", async (c) => { + const id = c.req.param("id"); + const body = await c.req.json<{ title?: string; keyId?: string; modelId?: string; status?: string }>(); + if (body.title !== undefined) updateTabTitle(id, body.title); + if (body.keyId !== undefined || body.modelId !== undefined) { + updateTabModel(id, body.keyId ?? null, body.modelId ?? null); + } + if (body.status !== undefined) updateTabStatus(id, body.status); + const tab = getTab(id); + return c.json(tab); +}); + +tabsRoutes.delete("/:id", (c) => { + const id = c.req.param("id"); + const mgr = getAgentManager(); + if (mgr) mgr.deleteTab(id); + archiveTab(id); + return c.json({ success: true }); +}); -- cgit v1.2.3