summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 04:09:00 +0900
committerAdam Malczewski <[email protected]>2026-05-22 04:09:00 +0900
commit8f1dd855f0c4c877bff8d3a4ba193432b268b1c2 (patch)
tree296a5641d8e84365875ecc0819282cfcf5502be0 /packages/core/src
parente43df9a50ed9ad25dc07a7d5ee90c66d14e8a16f (diff)
downloaddispatch-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.ts27
-rw-r--r--packages/core/src/db/tabs.ts77
-rw-r--r--packages/core/src/types/index.ts1
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 ──────────────────────────────────────────────────