From fb97d4cb72d0a90dde102b7001603716ee6e4c3b Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 22 May 2026 00:19:14 +0900 Subject: feat: agent summoning system, todo improvements, security fixes, double-execution bug fix - Add summon/retrieve tools for spawning child agents in new tabs - summon: non-blocking, returns agent_id immediately - retrieve: blocking, waits for child to finish, returns result - Child tools are intersected with parent permissions (no privilege escalation) - Working directory validated to stay within workspace - Abort controller stops orphaned agents on tab close - Rename task_list tool to todo with comprehensive usage guidance in system prompt - Rename PermissionLog.svelte to ToolPermissions.svelte - Add 'Summon agents' toggle to tool permissions UI - Redesign TaskListPanel with DaisyUI checkboxes (indeterminate for in-progress) - Remove 'blocked' status from task system - Add tab-created WebSocket event for child agent tab visibility - Add HMR cleanup for WebSocket connections (close stale connections on hot reload) - Fix ensureAssistantMessage to not throw on closed tabs - Fix double tool execution: remove execute from AI SDK tool() in registry.ts (agent.ts already executes tools manually via executeToolWithStreaming) - Fix all pre-existing test failures (missing mocks, stale API signatures) - Add debug info to copy button (tab ID, injected skills, all tab IDs) - Add tab ID and tools to conversation copy output --- .../src/lib/components/PermissionLog.svelte | 105 ------------------ .../src/lib/components/SidebarPanel.svelte | 120 +++++++++++---------- .../src/lib/components/TaskListPanel.svelte | 103 ++++++++++-------- .../src/lib/components/ToolPermissions.svelte | 118 ++++++++++++++++++++ packages/frontend/src/lib/settings.svelte.ts | 2 + packages/frontend/src/lib/tabs.svelte.ts | 36 ++++++- packages/frontend/src/lib/types.ts | 5 +- packages/frontend/src/lib/ws.svelte.ts | 15 +++ 8 files changed, 294 insertions(+), 210 deletions(-) delete mode 100644 packages/frontend/src/lib/components/PermissionLog.svelte create mode 100644 packages/frontend/src/lib/components/ToolPermissions.svelte (limited to 'packages/frontend/src/lib') diff --git a/packages/frontend/src/lib/components/PermissionLog.svelte b/packages/frontend/src/lib/components/PermissionLog.svelte deleted file mode 100644 index 7733dcf..0000000 --- a/packages/frontend/src/lib/components/PermissionLog.svelte +++ /dev/null @@ -1,105 +0,0 @@ - - -
-
Tool Permissions
-

Changes are applied when you send your next message.

- -
- {#each toolPermissions as perm (perm.id)} - - {/each} -
- - - -

Warning: changing tool access will reset the AI's prompt cache for active conversations, which may increase usage costs.

- - - {#if entries.length > 0} -
- -
- Log ({entries.length}) -
-
- {#each entries as entry (entry.id)} -
- - {entry.action} - - {entry.permission} - {entry.timestamp} -
-

{entry.description}

- {/each} -
-
- {/if} -
diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index 93d528e..a0aeb1d 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -1,64 +1,76 @@
@@ -115,7 +127,7 @@ {:else if panel.selected === "Skills"} {:else if panel.selected === "Tools"} - + {:else if panel.selected === "System Prompt"} {:else if panel.selected === "Settings"} diff --git a/packages/frontend/src/lib/components/TaskListPanel.svelte b/packages/frontend/src/lib/components/TaskListPanel.svelte index 5f2ffe2..d70373f 100644 --- a/packages/frontend/src/lib/components/TaskListPanel.svelte +++ b/packages/frontend/src/lib/components/TaskListPanel.svelte @@ -1,41 +1,45 @@
@@ -43,28 +47,35 @@

No tasks yet.

{:else}

- {tasks.length} task{tasks.length !== 1 ? "s" : ""} - ({doneCount} done, {inProgressCount} in progress) + {doneCount}/{tasks.length} done{#if inProgressCount > 0}, {inProgressCount} in progress{/if}

-
    +
      {#each tasks as task (task.id)} -
    • -
      - - {statusIcon(task.status)} - +
    • + +
      {task.title} + {#if task.description} +

      {task.description}

      + {/if}
      - {#if task.description} -

      {task.description}

      - {/if} -

      {task.id}

    • {/each}
    diff --git a/packages/frontend/src/lib/components/ToolPermissions.svelte b/packages/frontend/src/lib/components/ToolPermissions.svelte new file mode 100644 index 0000000..c6af47f --- /dev/null +++ b/packages/frontend/src/lib/components/ToolPermissions.svelte @@ -0,0 +1,118 @@ + + +
    +
    Tool Permissions
    +

    Changes are applied when you send your next message.

    + +
    + {#each toolPermissions as perm (perm.id)} + + {/each} +
    + + + +

    Warning: changing tool access will reset the AI's prompt cache for active conversations, which may increase usage costs.

    + + + {#if entries.length > 0} +
    + +
    + Log ({entries.length}) +
    +
    + {#each entries as entry (entry.id)} +
    + + {entry.action} + + {entry.permission} + {entry.timestamp} +
    +

    {entry.description}

    + {/each} +
    +
    + {/if} +
    diff --git a/packages/frontend/src/lib/settings.svelte.ts b/packages/frontend/src/lib/settings.svelte.ts index 2c8e2f5..1352a0c 100644 --- a/packages/frontend/src/lib/settings.svelte.ts +++ b/packages/frontend/src/lib/settings.svelte.ts @@ -7,12 +7,14 @@ let toolPerms = $state>({ read: true, edit: false, bash: false, + summon: false, external_directory: false, }); let savedToolPerms = $state>({ read: true, edit: false, bash: false, + summon: false, external_directory: false, }); let skillChecks = $state>({}); diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts index 55aa161..119df2d 100644 --- a/packages/frontend/src/lib/tabs.svelte.ts +++ b/packages/frontend/src/lib/tabs.svelte.ts @@ -95,7 +95,7 @@ function createTabStore() { activeTabId = id; // Auto-check default skills for injection with the first message - autoCheckDefaultSkills(); + void autoCheckDefaultSkills(); return tab; } @@ -133,9 +133,9 @@ function createTabStore() { tabs = tabs.map((t) => (t.id === id ? { ...t, ...patch } : t)); } - function ensureAssistantMessage(tabId: string): ChatMessage { + function ensureAssistantMessage(tabId: string): ChatMessage | null { const tab = getTabById(tabId); - if (!tab) throw new Error(`Tab not found: ${tabId}`); + if (!tab) return null; if (tab.currentAssistantId) { const existing = tab.messages.find((m) => m.id === tab.currentAssistantId); @@ -333,6 +333,26 @@ function createTabStore() { ); break; } + case "tab-created": { + const newTabEvent = event as AgentEvent & { id: string; title: string }; + // Only add if we don't already have this tab + if (!getTabById(newTabEvent.id)) { + const tab: Tab = { + id: newTabEvent.id, + title: newTabEvent.title, + messages: [], + agentStatus: "running", + keyId: null, + modelId: null, + reasoningEffort: "max", + currentAssistantId: null, + tasks: [], + injectedSkills: [], + }; + tabs = [...tabs, tab]; + } + break; + } } } @@ -543,10 +563,20 @@ function createTabStore() { function copyConversation(): string { const tab = getActiveTab(); if (!tab) return ""; + + const enabledTools = Object.entries(appSettings.savedToolPerms) + .filter(([, v]) => v) + .map(([k]) => k); + const lines: string[] = [ "=== Dispatch Conversation ===", + `Tab ID: ${tab.id}`, `Tab: ${tab.title}`, `Model: ${tab.modelId ?? "default"}`, + `Tools: ${enabledTools.length > 0 ? enabledTools.join(", ") : "none"}`, + `Injected Skills: ${tab.injectedSkills.length > 0 ? tab.injectedSkills.join(", ") : "none"}`, + `Total tabs: ${tabs.length}`, + `All tab IDs: ${tabs.map((t) => t.id).join(", ")}`, "", ]; for (const msg of tab.messages) { diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index 28752a0..a1ec24b 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -63,13 +63,14 @@ export type AgentEvent = }; } | { type: "permission-prompt"; pending: PermissionPrompt[] } - | { type: "shell-output"; data: string; stream: "stdout" | "stderr" }; + | { type: "shell-output"; data: string; stream: "stdout" | "stderr" } + | { type: "tab-created"; id: string; title: string }; export interface TaskItem { id: string; title: string; description: string; - status: "pending" | "in_progress" | "done" | "blocked"; + status: "pending" | "in_progress" | "done"; } export interface PermissionPrompt { diff --git a/packages/frontend/src/lib/ws.svelte.ts b/packages/frontend/src/lib/ws.svelte.ts index 95243d5..0970311 100644 --- a/packages/frontend/src/lib/ws.svelte.ts +++ b/packages/frontend/src/lib/ws.svelte.ts @@ -3,6 +3,16 @@ import type { AgentEvent, ConnectionStatus } from "./types.js"; type EventCallback = (event: AgentEvent) => void; +// Close any stale WebSocket from HMR reloads +if (import.meta.hot) { + import.meta.hot.dispose((data: Record) => { + const old = data._ws as WebSocket | undefined; + if (old && old.readyState === WebSocket.OPEN) { + old.close(); + } + }); +} + function createWebSocketClient(url: string) { let connectionStatus: ConnectionStatus = $state("disconnected"); let ws: WebSocket | null = null; @@ -19,6 +29,11 @@ function createWebSocketClient(url: string) { connectionStatus = "connecting"; ws = new WebSocket(url); + // Store ref for HMR cleanup + if (import.meta.hot) { + import.meta.hot.data._ws = ws; + } + ws.onopen = () => { connectionStatus = "connected"; reconnectDelay = 1000; -- cgit v1.2.3