summaryrefslogtreecommitdiffhomepage
path: root/packages/api/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/api/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/api/src')
-rw-r--r--packages/api/src/agent-manager.ts72
1 files changed, 55 insertions, 17 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 });
+ }
}
}