summaryrefslogtreecommitdiffhomepage
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
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)
-rw-r--r--packages/api/src/agent-manager.ts72
-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
-rw-r--r--packages/frontend/src/lib/components/TabBar.svelte55
-rw-r--r--packages/frontend/src/lib/components/ToolCallDisplay.svelte23
-rw-r--r--packages/frontend/src/lib/tabs.svelte.ts98
-rw-r--r--packages/frontend/src/lib/types.ts1
-rw-r--r--packages/frontend/vite.config.ts1
9 files changed, 309 insertions, 46 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
index e4f284d..7e814af 100644
--- a/packages/api/src/agent-manager.ts
+++ b/packages/api/src/agent-manager.ts
@@ -338,6 +338,7 @@ export class AgentManager {
parentKeyId: tabAgent.keyId,
parentModelId: tabAgent.modelId,
parentAllowedTools: childParentAllowedTools,
+ parentTabId: tabId,
}),
}),
});
@@ -373,6 +374,7 @@ export class AgentManager {
parentKeyId: tabAgent.keyId,
parentModelId: tabAgent.modelId,
parentAllowedTools,
+ parentTabId: tabId,
}),
}),
});
@@ -566,6 +568,7 @@ export class AgentManager {
parentKeyId?: string | null;
parentModelId?: string | null;
parentAllowedTools?: Set<string>;
+ parentTabId?: string;
}): Promise<string> {
const tabId = crypto.randomUUID();
const title = options.task.length > 50 ? `${options.task.slice(0, 47)}...` : options.task;
@@ -605,14 +608,25 @@ export class AgentManager {
// Create tab in DB
try {
const { createTab } = await import("@dispatch/core");
- createTab(tabId, title);
+ createTab(tabId, title, {
+ keyId: tabAgent.keyId,
+ modelId: tabAgent.modelId,
+ parentTabId: options.parentTabId,
+ });
} catch {
// Continue even if DB fails
}
// Notify the frontend about the new tab
this.emit(
- { type: "tab-created", id: tabId, title, keyId: tabAgent.keyId, modelId: tabAgent.modelId },
+ {
+ type: "tab-created",
+ id: tabId,
+ title,
+ keyId: tabAgent.keyId,
+ modelId: tabAgent.modelId,
+ parentTabId: options.parentTabId ?? null,
+ },
tabId,
);
@@ -668,6 +682,18 @@ export class AgentManager {
tabAgent.status = "running";
this.messageCount += 1;
+ let allOutput = "";
+ let assistantText = "";
+ let assistantThinking = "";
+ const assistantToolCalls: Array<{
+ id: string;
+ name: string;
+ arguments: Record<string, unknown>;
+ result?: string;
+ isError?: boolean;
+ }> = [];
+ let processError: string | null = null;
+
try {
const agent = await this.getOrCreateAgentForTab(tabId, keyId, modelId);
@@ -679,17 +705,6 @@ export class AgentManager {
JSON.stringify([{ type: "text", text: message }]),
);
- let allOutput = "";
- let assistantText = "";
- let assistantThinking = "";
- const assistantToolCalls: Array<{
- id: string;
- name: string;
- arguments: Record<string, unknown>;
- result?: string;
- isError?: boolean;
- }> = [];
-
for await (const event of agent.run(
message,
reasoningEffort ? { reasoningEffort } : undefined,
@@ -742,15 +757,38 @@ export class AgentManager {
assistantToolCalls.length = 0;
}
}
- // Resolve completion promise for child agents
- tabAgent.finalOutput = allOutput;
- tabAgent.completionResolve?.({ status: "done", result: allOutput || "(no output)" });
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
+ processError = errorMsg;
tabAgent.status = "error";
this.emit({ type: "error", error: errorMsg }, tabId);
this.emit({ type: "status", status: "error" }, tabId);
- tabAgent.completionResolve?.({ status: "error", error: errorMsg });
+ } finally {
+ // Flush any accumulated assistant content that wasn't saved by a done event
+ // (happens when the agent is aborted mid-stream or throws an error)
+ if (assistantText || assistantToolCalls.length > 0) {
+ const contentSegments: Array<Record<string, unknown>> = [];
+ if (assistantText) contentSegments.push({ type: "text", text: assistantText });
+ for (const tc of assistantToolCalls) {
+ contentSegments.push({ type: "tool-call", ...tc });
+ }
+ if (contentSegments.length > 0) {
+ appendMessage(
+ tabId,
+ crypto.randomUUID(),
+ "assistant",
+ JSON.stringify(contentSegments),
+ assistantThinking || undefined,
+ );
+ }
+ }
+ // Resolve completion promise for child agents
+ if (processError === null) {
+ tabAgent.finalOutput = allOutput;
+ tabAgent.completionResolve?.({ status: "done", result: allOutput || "(no output)" });
+ } else {
+ tabAgent.completionResolve?.({ status: "error", error: processError });
+ }
}
}
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 ──────────────────────────────────────────────────
diff --git a/packages/frontend/src/lib/components/TabBar.svelte b/packages/frontend/src/lib/components/TabBar.svelte
index b5597e1..cfa5887 100644
--- a/packages/frontend/src/lib/components/TabBar.svelte
+++ b/packages/frontend/src/lib/components/TabBar.svelte
@@ -6,8 +6,23 @@ function statusColor(status: string): string {
if (status === "error") return "bg-error";
return "bg-success";
}
+
+const userTabs = $derived(tabStore.tabs.filter((t) => t.parentTabId === null));
+const subagentTabs = $derived(
+ tabStore.tabs.filter((t) => t.parentTabId !== null && t.parentTabId === activeUserTabId),
+);
+const hasSubagentTabs = $derived(subagentTabs.length > 0);
+
+// When a subagent tab is active, its parent user tab should still appear selected
+const activeTab = $derived(tabStore.tabs.find((t) => t.id === tabStore.activeTabId));
+const activeUserTabId = $derived(
+ activeTab?.parentTabId !== null && activeTab?.parentTabId !== undefined
+ ? activeTab.parentTabId
+ : tabStore.activeTabId,
+);
</script>
+<!-- Top row: user tabs -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="overflow-x-auto bg-base-200 flex-shrink-0"
@@ -29,11 +44,11 @@ function statusColor(status: string): string {
+
</button>
- {#each tabStore.tabs as tab (tab.id)}
+ {#each userTabs as tab (tab.id)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
role="tab"
- class="tab !flex items-stretch gap-1.5 {tab.id === tabStore.activeTabId ? 'tab-active' : ''}"
+ class="tab !flex items-stretch gap-1.5 {tab.id === activeUserTabId ? 'tab-active' : ''}"
onclick={() => tabStore.switchTab(tab.id)}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') tabStore.switchTab(tab.id); }}
tabindex="0"
@@ -54,3 +69,39 @@ function statusColor(status: string): string {
{/each}
</div>
</div>
+
+<!-- Bottom row: subagent tabs (hidden when empty) -->
+{#if hasSubagentTabs}
+ <div class="overflow-x-auto bg-base-200 flex-shrink-0 border-t border-base-300">
+ <div
+ role="tablist"
+ class="tabs tabs-lift tabs-xs min-w-max"
+ >
+ {#each subagentTabs as tab (tab.id)}
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <div
+ role="tab"
+ class="tab !flex items-stretch gap-1 {tab.id === tabStore.activeTabId ? 'tab-active' : ''} {!tab.persistent ? 'opacity-70 italic' : ''}"
+ onclick={() => tab.persistent ? tabStore.switchTab(tab.id) : tabStore.promoteTab(tab.id)}
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') tab.persistent ? tabStore.switchTab(tab.id) : tabStore.promoteTab(tab.id); }}
+ tabindex="0"
+ >
+ <span class="flex items-center gap-1">
+ <span class="w-1 h-1 rounded-full shrink-0 {statusColor(tab.agentStatus)}"></span>
+ <span class="max-w-28 truncate text-xs">{tab.title}</span>
+ </span>
+ {#if tab.persistent}
+ <button
+ type="button"
+ class="flex items-center justify-center px-2 my-0.5 leading-none text-base-content/30 hover:text-error hover:bg-base-300 rounded transition-colors text-xs"
+ onclick={(e) => { e.stopPropagation(); tabStore.closeTab(tab.id); }}
+ aria-label="Close tab"
+ >
+ &#x2715;
+ </button>
+ {/if}
+ </div>
+ {/each}
+ </div>
+ </div>
+{/if}
diff --git a/packages/frontend/src/lib/components/ToolCallDisplay.svelte b/packages/frontend/src/lib/components/ToolCallDisplay.svelte
index 805de6d..213ba17 100644
--- a/packages/frontend/src/lib/components/ToolCallDisplay.svelte
+++ b/packages/frontend/src/lib/components/ToolCallDisplay.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
+import { tabStore } from "../tabs.svelte.js";
import type { ToolCallDisplay } from "../types.js";
const { toolCall }: { toolCall: ToolCallDisplay } = $props();
@@ -41,17 +42,33 @@ const isShell = $derived(toolCall.name === "run_shell");
const shellResult = $derived(
isShell && toolCall.result !== undefined ? parseShellResult(toolCall.result) : null,
);
+
+const summonAgentId = $derived.by(() => {
+ if (toolCall.name !== "summon" || !toolCall.result) return null;
+ const match = toolCall.result.match(/agent_id:\s*([a-f0-9-]+)/);
+ return match ? match[1] : null;
+});
</script>
<div class="collapse collapse-arrow mb-2 p-1 opacity-60 {isExpanded ? 'collapse-open' : ''}">
- <button
- type="button"
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <div
class="collapse-title flex items-center gap-2 text-sm italic cursor-pointer w-full text-left"
onclick={toggle}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') toggle(); }}
aria-expanded={isExpanded}
>
<span class="badge badge-neutral badge-sm">tool</span>
<span class="font-mono">{toolCall.name}</span>
+ {#if summonAgentId !== null}
+ <button
+ type="button"
+ class="btn btn-xs btn-ghost"
+ onclick={(e) => { e.stopPropagation(); tabStore.openAgentTab(summonAgentId!); }}
+ >Open Tab</button>
+ {/if}
{#if toolCall.result !== undefined}
{#if isShell && shellResult !== null}
<span class="badge badge-sm ml-auto {shellResult.exitCode === 0 ? 'badge-success' : 'badge-error'}">
@@ -65,7 +82,7 @@ const shellResult = $derived(
{:else}
<span class="badge badge-warning badge-sm ml-auto">pending</span>
{/if}
- </button>
+ </div>
<div class="collapse-content text-xs">
<div class="mt-2">
diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts
index 9615c3d..15a0736 100644
--- a/packages/frontend/src/lib/tabs.svelte.ts
+++ b/packages/frontend/src/lib/tabs.svelte.ts
@@ -34,6 +34,10 @@ export interface Tab {
currentAssistantId: string | null;
tasks: TaskItem[];
injectedSkills: string[];
+ /** null = user-owned tab, string = spawned by that tab */
+ parentTabId: string | null;
+ /** Persistent tabs stay until manually closed. Temp tabs disappear when agent finishes. */
+ persistent: boolean;
}
function createTabStore() {
@@ -90,6 +94,8 @@ function createTabStore() {
currentAssistantId: null,
tasks: [],
injectedSkills: [],
+ parentTabId: null,
+ persistent: true,
};
tabs = [...tabs, tab];
activeTabId = id;
@@ -106,6 +112,83 @@ function createTabStore() {
}
}
+ function promoteTab(id: string): void {
+ const tab = getTabById(id);
+ if (!tab) return;
+ updateTab(id, { persistent: true });
+ switchTab(id);
+ }
+
+ async function openAgentTab(agentId: string): Promise<void> {
+ const tab = getTabById(agentId);
+ if (tab) {
+ updateTab(agentId, { persistent: true });
+ switchTab(agentId);
+ return;
+ }
+
+ // Tab not found locally — try to fetch from backend
+ try {
+ const tabRes = await fetch(`${config.apiBase}/tabs/${agentId}`);
+ if (!tabRes.ok) return; // 404 or other error — tab doesn't exist
+ const tabData = (await tabRes.json()) as {
+ id: string;
+ title: string;
+ keyId?: string | null;
+ modelId?: string | null;
+ status?: string;
+ parentTabId?: string | null;
+ };
+
+ const messagesRes = await fetch(`${config.apiBase}/tabs/${agentId}/messages`);
+ const messagesData = messagesRes.ok
+ ? ((await messagesRes.json()) as {
+ messages: Array<{
+ id?: string;
+ role: string;
+ contentJson: string;
+ thinking: string | null;
+ }>;
+ })
+ : { messages: [] };
+
+ const chatMessages: ChatMessage[] = messagesData.messages.flatMap((m) => {
+ try {
+ return [
+ {
+ id: m.id ?? generateId(),
+ role: m.role as ChatMessage["role"],
+ content: JSON.parse(m.contentJson) as ContentSegment[],
+ thinking: m.thinking ?? undefined,
+ isStreaming: false,
+ },
+ ];
+ } catch {
+ return [];
+ }
+ });
+
+ const newTab: Tab = {
+ id: agentId,
+ title: tabData.title,
+ messages: chatMessages,
+ agentStatus: "idle",
+ keyId: tabData.keyId ?? null,
+ modelId: tabData.modelId ?? null,
+ reasoningEffort: "max",
+ currentAssistantId: null,
+ tasks: [],
+ injectedSkills: [],
+ parentTabId: tabData.parentTabId ?? null,
+ persistent: true,
+ };
+ tabs = [...tabs, newTab];
+ activeTabId = agentId;
+ } catch (err) {
+ console.error("openAgentTab failed:", err);
+ }
+ }
+
async function closeTab(id: string): Promise<void> {
const tab = getTabById(id);
if (!tab) return;
@@ -122,7 +205,11 @@ function createTabStore() {
// If we closed the active tab, switch to the last remaining or create a new one
if (activeTabId === id) {
if (tabs.length > 0) {
- activeTabId = tabs[tabs.length - 1]?.id;
+ const fallback = tabs[tabs.length - 1];
+ if (fallback && !fallback.persistent) {
+ updateTab(fallback.id, { persistent: true });
+ }
+ activeTabId = fallback?.id;
} else {
await createNewTab();
}
@@ -172,6 +259,10 @@ function createTabStore() {
updateTab(tabId, { agentStatus: event.status });
if (event.status === "idle" || event.status === "error") {
updateTab(tabId, { currentAssistantId: null });
+ const tab = getTabById(tabId);
+ if (tab && !tab.persistent && tabId !== activeTabId) {
+ tabs = tabs.filter((t) => t.id !== tabId);
+ }
}
}
break;
@@ -339,6 +430,7 @@ function createTabStore() {
title: string;
keyId: string | null;
modelId: string | null;
+ parentTabId: string | null;
};
// Only add if we don't already have this tab
if (!getTabById(newTabEvent.id)) {
@@ -353,6 +445,8 @@ function createTabStore() {
currentAssistantId: null,
tasks: [],
injectedSkills: [],
+ parentTabId: newTabEvent.parentTabId ?? null,
+ persistent: newTabEvent.parentTabId == null,
};
tabs = [...tabs, tab];
}
@@ -630,6 +724,8 @@ function createTabStore() {
setKey,
replyPermission,
copyConversation,
+ promoteTab,
+ openAgentTab,
};
}
diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts
index e810864..4523828 100644
--- a/packages/frontend/src/lib/types.ts
+++ b/packages/frontend/src/lib/types.ts
@@ -70,6 +70,7 @@ export type AgentEvent =
title: string;
keyId: string | null;
modelId: string | null;
+ parentTabId: string | null;
};
export interface TaskItem {
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 3604d5e..ca947d0 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -6,6 +6,7 @@ export default defineConfig({
plugins: [tailwindcss(), svelte()],
server: {
port: 5173,
+ allowedHosts: true,
},
test: {
include: ["tests/**/*.test.ts"],