diff options
| author | Adam Malczewski <[email protected]> | 2026-05-22 04:09:00 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-22 04:09:00 +0900 |
| commit | 8f1dd855f0c4c877bff8d3a4ba193432b268b1c2 (patch) | |
| tree | 296a5641d8e84365875ecc0819282cfcf5502be0 /packages/core/src | |
| parent | e43df9a50ed9ad25dc07a7d5ee90c66d14e8a16f (diff) | |
| download | dispatch-8f1dd855f0c4c877bff8d3a4ba193432b268b1c2.tar.gz dispatch-8f1dd855f0c4c877bff8d3a4ba193432b268b1c2.zip | |
feat: two-row tab bar with temp/persistent subagent tabs, tab persistence
- Split tab bar into user tabs (top) and subagent tabs (bottom row)
- Bottom row only visible when active user tab has child agents
- Parent user tab stays highlighted when viewing a subagent tab
- Temp tabs auto-disappear when agent completes, click to promote to persistent
- 'Open Tab' button on summon tool calls to view/reopen subagent history
- Reopening archived tabs fetches full chat history from backend
- Add parent_tab_id column to DB with safe ALTER TABLE migration
- Persist keyId, modelId, parentTabId on child tab creation
- Flush partial assistant messages on abort/error (finally block)
- Defensive JSON.parse in message restoration
- Allow all Vite hosts for local development
- Fix nested button in ToolCallDisplay (button inside button)
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/db/index.ts | 27 | ||||
| -rw-r--r-- | packages/core/src/db/tabs.ts | 77 | ||||
| -rw-r--r-- | packages/core/src/types/index.ts | 1 |
3 files changed, 82 insertions, 23 deletions
diff --git a/packages/core/src/db/index.ts b/packages/core/src/db/index.ts index 2992708..81b474e 100644 --- a/packages/core/src/db/index.ts +++ b/packages/core/src/db/index.ts @@ -1,7 +1,7 @@ import { Database } from "bun:sqlite"; import { existsSync, mkdirSync } from "node:fs"; -import { isAbsolute, join } from "node:path"; import { homedir } from "node:os"; +import { isAbsolute, join } from "node:path"; /** * Returns the directory for persistent Dispatch data, following XDG Base @@ -75,17 +75,24 @@ export function getDatabase(): Database { )`); _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 + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + key_id TEXT, + model_id TEXT, + parent_tab_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 )`); + try { + _db.run("ALTER TABLE tabs ADD COLUMN parent_tab_id TEXT"); + } catch { + // Column already exists — ignore + } + _db.run(`CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, tab_id TEXT NOT NULL REFERENCES tabs(id), diff --git a/packages/core/src/db/tabs.ts b/packages/core/src/db/tabs.ts index 0f4511f..c0ce809 100644 --- a/packages/core/src/db/tabs.ts +++ b/packages/core/src/db/tabs.ts @@ -5,6 +5,7 @@ export interface TabRow { title: string; keyId: string | null; modelId: string | null; + parentTabId: string | null; status: string; isOpen: boolean; position: number; @@ -18,6 +19,7 @@ function rowToTab(row: Record<string, unknown>): TabRow { title: row.title as string, keyId: row.key_id as string | null, modelId: row.model_id as string | null, + parentTabId: (row.parent_tab_id as string) ?? null, status: row.status as string, isOpen: (row.is_open as number) === 1, position: row.position as number, @@ -26,48 +28,97 @@ function rowToTab(row: Record<string, unknown>): TabRow { }; } -export function createTab(id: string, title: string): TabRow { +export function createTab( + id: string, + title: string, + options?: { keyId?: string | null; modelId?: string | null; parentTabId?: string | null }, +): 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 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; + const keyId = options?.keyId ?? null; + const modelId = options?.modelId ?? null; + const parentTabId = options?.parentTabId ?? null; 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 }; + `INSERT INTO tabs (id, title, key_id, model_id, parent_tab_id, status, is_open, position, created_at, updated_at) + VALUES ($id, $title, $keyId, $modelId, $parentTabId, 'idle', 1, $position, $now, $now)`, + ).run({ + $id: id, + $title: title, + $keyId: keyId, + $modelId: modelId, + $parentTabId: parentTabId, + $position: position, + $now: now, + }); + return { + id, + title, + keyId, + modelId, + parentTabId, + 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; + 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>>; + 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() }); + 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(), + 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() }); + 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() }); + 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/types/index.ts b/packages/core/src/types/index.ts index 8745bc7..eaf8669 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -46,6 +46,7 @@ export type AgentEvent = title: string; keyId: string | null; modelId: string | null; + parentTabId: string | null; }; // ─── Tool Types ────────────────────────────────────────────────── |
