summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-21 19:52:35 +0900
committerAdam Malczewski <[email protected]>2026-05-21 19:52:35 +0900
commit4ba2673557c7cfd0bc31b03e2c35ab5e1efe60e7 (patch)
treedddae8fc37a12157f4c8e840475e9e5d6203464a /packages/core/src
parent55633c90c0d96e62153a4b6655f3f833a3b46ad4 (diff)
downloaddispatch-4ba2673557c7cfd0bc31b03e2c35ab5e1efe60e7.tar.gz
dispatch-4ba2673557c7cfd0bc31b03e2c35ab5e1efe60e7.zip
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<tabId, TabAgent> - 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)
Diffstat (limited to 'packages/core/src')
-rw-r--r--packages/core/src/db/index.ts29
-rw-r--r--packages/core/src/db/messages.ts47
-rw-r--r--packages/core/src/db/settings.ts20
-rw-r--r--packages/core/src/db/tabs.ts73
-rw-r--r--packages/core/src/index.ts5
5 files changed, 174 insertions, 0 deletions
diff --git a/packages/core/src/db/index.ts b/packages/core/src/db/index.ts
index 818aff0..2992708 100644
--- a/packages/core/src/db/index.ts
+++ b/packages/core/src/db/index.ts
@@ -74,6 +74,35 @@ export function getDatabase(): Database {
updated_at INTEGER NOT NULL
)`);
+ _db.run(`CREATE TABLE IF NOT EXISTS tabs (
+ id TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ key_id TEXT,
+ model_id TEXT,
+ status TEXT NOT NULL DEFAULT 'idle',
+ is_open INTEGER NOT NULL DEFAULT 1,
+ position INTEGER NOT NULL DEFAULT 0,
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL
+ )`);
+
+ _db.run(`CREATE TABLE IF NOT EXISTS messages (
+ id TEXT PRIMARY KEY,
+ tab_id TEXT NOT NULL REFERENCES tabs(id),
+ seq INTEGER NOT NULL,
+ role TEXT NOT NULL,
+ content_json TEXT NOT NULL,
+ thinking TEXT,
+ created_at INTEGER NOT NULL
+ )`);
+
+ _db.run(`CREATE INDEX IF NOT EXISTS idx_messages_tab ON messages(tab_id, seq)`);
+
+ _db.run(`CREATE TABLE IF NOT EXISTS settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ )`);
+
return _db;
}
diff --git a/packages/core/src/db/messages.ts b/packages/core/src/db/messages.ts
new file mode 100644
index 0000000..5a8758f
--- /dev/null
+++ b/packages/core/src/db/messages.ts
@@ -0,0 +1,47 @@
+import { getDatabase } from "./index.js";
+
+export interface MessageRow {
+ id: string;
+ tabId: string;
+ seq: number;
+ role: string;
+ contentJson: string;
+ thinking: string | null;
+ createdAt: number;
+}
+
+export function appendMessage(tabId: string, id: string, role: string, contentJson: string, thinking?: string): void {
+ const db = getDatabase();
+ const maxSeq = db.query("SELECT COALESCE(MAX(seq), -1) as max_seq FROM messages WHERE tab_id = $tabId").get({ $tabId: tabId }) as { max_seq: number };
+ const seq = (maxSeq?.max_seq ?? -1) + 1;
+ db.query(
+ `INSERT INTO messages (id, tab_id, seq, role, content_json, thinking, created_at)
+ VALUES ($id, $tabId, $seq, $role, $contentJson, $thinking, $now)`,
+ ).run({ $id: id, $tabId: tabId, $seq: seq, $role: role, $contentJson: contentJson, $thinking: thinking ?? null, $now: Date.now() });
+}
+
+export function updateMessage(id: string, contentJson: string, thinking?: string): void {
+ const db = getDatabase();
+ db.query(
+ "UPDATE messages SET content_json = $contentJson, thinking = $thinking WHERE id = $id",
+ ).run({ $id: id, $contentJson: contentJson, $thinking: thinking ?? null });
+}
+
+export function getMessagesForTab(tabId: string): MessageRow[] {
+ const db = getDatabase();
+ const rows = db.query("SELECT * FROM messages WHERE tab_id = $tabId ORDER BY seq ASC").all({ $tabId: tabId }) as Array<Record<string, unknown>>;
+ return rows.map((row) => ({
+ id: row.id as string,
+ tabId: row.tab_id as string,
+ seq: row.seq as number,
+ role: row.role as string,
+ contentJson: row.content_json as string,
+ thinking: row.thinking as string | null,
+ createdAt: row.created_at as number,
+ }));
+}
+
+export function clearMessagesForTab(tabId: string): void {
+ const db = getDatabase();
+ db.query("DELETE FROM messages WHERE tab_id = $tabId").run({ $tabId: tabId });
+}
diff --git a/packages/core/src/db/settings.ts b/packages/core/src/db/settings.ts
new file mode 100644
index 0000000..51d6ea2
--- /dev/null
+++ b/packages/core/src/db/settings.ts
@@ -0,0 +1,20 @@
+import { getDatabase } from "./index.js";
+
+export function getSetting(key: string): string | null {
+ const db = getDatabase();
+ const row = db.query("SELECT value FROM settings WHERE key = $key").get({ $key: key }) as { value: string } | null;
+ return row?.value ?? null;
+}
+
+export function setSetting(key: string, value: string): void {
+ const db = getDatabase();
+ db.query(
+ `INSERT INTO settings (key, value) VALUES ($key, $value)
+ ON CONFLICT(key) DO UPDATE SET value = $value`,
+ ).run({ $key: key, $value: value });
+}
+
+export function deleteSetting(key: string): void {
+ const db = getDatabase();
+ db.query("DELETE FROM settings WHERE key = $key").run({ $key: key });
+}
diff --git a/packages/core/src/db/tabs.ts b/packages/core/src/db/tabs.ts
new file mode 100644
index 0000000..0f4511f
--- /dev/null
+++ b/packages/core/src/db/tabs.ts
@@ -0,0 +1,73 @@
+import { getDatabase } from "./index.js";
+
+export interface TabRow {
+ id: string;
+ title: string;
+ keyId: string | null;
+ modelId: string | null;
+ status: string;
+ isOpen: boolean;
+ position: number;
+ createdAt: number;
+ updatedAt: number;
+}
+
+function rowToTab(row: Record<string, unknown>): TabRow {
+ return {
+ id: row.id as string,
+ title: row.title as string,
+ keyId: row.key_id as string | null,
+ modelId: row.model_id as string | null,
+ status: row.status as string,
+ isOpen: (row.is_open as number) === 1,
+ position: row.position as number,
+ createdAt: row.created_at as number,
+ updatedAt: row.updated_at as number,
+ };
+}
+
+export function createTab(id: string, title: string): TabRow {
+ const db = getDatabase();
+ const now = Date.now();
+ const maxPos = db.query("SELECT COALESCE(MAX(position), -1) as max_pos FROM tabs WHERE is_open = 1").get() as { max_pos: number };
+ const position = (maxPos?.max_pos ?? -1) + 1;
+ db.query(
+ `INSERT INTO tabs (id, title, key_id, model_id, status, is_open, position, created_at, updated_at)
+ VALUES ($id, $title, NULL, NULL, 'idle', 1, $position, $now, $now)`,
+ ).run({ $id: id, $title: title, $position: position, $now: now });
+ return { id, title, keyId: null, modelId: null, status: "idle", isOpen: true, position, createdAt: now, updatedAt: now };
+}
+
+export function getTab(id: string): TabRow | null {
+ const db = getDatabase();
+ const row = db.query("SELECT * FROM tabs WHERE id = $id").get({ $id: id }) as Record<string, unknown> | null;
+ return row ? rowToTab(row) : null;
+}
+
+export function listOpenTabs(): TabRow[] {
+ const db = getDatabase();
+ const rows = db.query("SELECT * FROM tabs WHERE is_open = 1 ORDER BY position ASC").all() as Array<Record<string, unknown>>;
+ return rows.map(rowToTab);
+}
+
+export function updateTabTitle(id: string, title: string): void {
+ const db = getDatabase();
+ db.query("UPDATE tabs SET title = $title, updated_at = $now WHERE id = $id").run({ $id: id, $title: title, $now: Date.now() });
+}
+
+export function updateTabModel(id: string, keyId: string | null, modelId: string | null): void {
+ const db = getDatabase();
+ db.query("UPDATE tabs SET key_id = $keyId, model_id = $modelId, updated_at = $now WHERE id = $id").run({
+ $id: id, $keyId: keyId, $modelId: modelId, $now: Date.now(),
+ });
+}
+
+export function updateTabStatus(id: string, status: string): void {
+ const db = getDatabase();
+ db.query("UPDATE tabs SET status = $status, updated_at = $now WHERE id = $id").run({ $id: id, $status: status, $now: Date.now() });
+}
+
+export function archiveTab(id: string): void {
+ const db = getDatabase();
+ db.query("UPDATE tabs SET is_open = 0, updated_at = $now WHERE id = $id").run({ $id: id, $now: Date.now() });
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index b1e2e14..3b49de9 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -32,3 +32,8 @@ export * from "./credentials/index.js";
// Database
export { getDatabase, closeDatabase, getDatabasePath } from "./db/index.js";
+
+// Tabs & Messages
+export { type TabRow, createTab, getTab, listOpenTabs, updateTabTitle, updateTabModel, updateTabStatus, archiveTab } from "./db/tabs.js";
+export { type MessageRow, appendMessage, updateMessage, getMessagesForTab, clearMessagesForTab } from "./db/messages.js";
+export { getSetting, setSetting, deleteSetting } from "./db/settings.js";