summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components
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/frontend/src/lib/components
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/frontend/src/lib/components')
-rw-r--r--packages/frontend/src/lib/components/TabBar.svelte55
-rw-r--r--packages/frontend/src/lib/components/ToolCallDisplay.svelte23
2 files changed, 73 insertions, 5 deletions
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">