summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 00:19:14 +0900
committerAdam Malczewski <[email protected]>2026-05-22 00:19:14 +0900
commitfb97d4cb72d0a90dde102b7001603716ee6e4c3b (patch)
tree55c6e8b56b3395008523ab94c16c4f526083d846 /packages/frontend/src/lib/components
parent7884709e3b2adb1b65c1c086257e0300eed51cee (diff)
downloaddispatch-fb97d4cb72d0a90dde102b7001603716ee6e4c3b.tar.gz
dispatch-fb97d4cb72d0a90dde102b7001603716ee6e4c3b.zip
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
Diffstat (limited to 'packages/frontend/src/lib/components')
-rw-r--r--packages/frontend/src/lib/components/SidebarPanel.svelte120
-rw-r--r--packages/frontend/src/lib/components/TaskListPanel.svelte103
-rw-r--r--packages/frontend/src/lib/components/ToolPermissions.svelte (renamed from packages/frontend/src/lib/components/PermissionLog.svelte)21
3 files changed, 140 insertions, 104 deletions
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 @@
<script lang="ts">
- import ModelSelector from "./ModelSelector.svelte";
- import ModelStatus from "./ModelStatus.svelte";
- import TaskListPanel from "./TaskListPanel.svelte";
- import ConfigPanel from "./ConfigPanel.svelte";
- import SkillsBrowser from "./SkillsBrowser.svelte";
- import PermissionLog from "./PermissionLog.svelte";
- import KeyUsage from "./KeyUsage.svelte";
- import ClaudeReset from "./ClaudeReset.svelte";
- import SettingsPanel from "./SettingsPanel.svelte";
- import SystemPromptPanel from "./SystemPromptPanel.svelte";
- import type { TaskItem, LogEntry, KeyInfo } from "../types.js";
+import type { KeyInfo, LogEntry, TaskItem } from "../types.js";
+import ClaudeReset from "./ClaudeReset.svelte";
+import ConfigPanel from "./ConfigPanel.svelte";
+import KeyUsage from "./KeyUsage.svelte";
+import ModelSelector from "./ModelSelector.svelte";
+import ModelStatus from "./ModelStatus.svelte";
+import SettingsPanel from "./SettingsPanel.svelte";
+import SkillsBrowser from "./SkillsBrowser.svelte";
+import SystemPromptPanel from "./SystemPromptPanel.svelte";
+import TaskListPanel from "./TaskListPanel.svelte";
+import ToolPermissions from "./ToolPermissions.svelte";
- const {
- keys = [],
- tasks = [],
- permissionLog = [],
- apiBase = "",
- activeKeyId = null,
- activeModelId = null,
- reasoningEffort = "max",
- onKeyChange,
- onModelChange,
- onReasoningChange,
- }: {
- keys?: KeyInfo[];
- tasks?: TaskItem[];
- permissionLog?: LogEntry[];
- apiBase?: string;
- activeKeyId?: string | null;
- activeModelId?: string | null;
- reasoningEffort?: string;
- onKeyChange: (keyId: string) => void;
- onModelChange: (keyId: string, modelId: string) => void;
- onReasoningChange: (effort: string) => void;
- } = $props();
+const {
+ keys = [],
+ tasks = [],
+ permissionLog = [],
+ apiBase = "",
+ activeKeyId = null,
+ activeModelId = null,
+ reasoningEffort = "max",
+ onKeyChange,
+ onModelChange,
+ onReasoningChange,
+}: {
+ keys?: KeyInfo[];
+ tasks?: TaskItem[];
+ permissionLog?: LogEntry[];
+ apiBase?: string;
+ activeKeyId?: string | null;
+ activeModelId?: string | null;
+ reasoningEffort?: string;
+ onKeyChange: (keyId: string) => void;
+ onModelChange: (keyId: string, modelId: string) => void;
+ onReasoningChange: (effort: string) => void;
+} = $props();
- interface Panel {
- id: number;
- selected: string;
- }
+interface Panel {
+ id: number;
+ selected: string;
+}
- let nextId = 0;
- let panels = $state<Panel[]>([{ id: nextId++, selected: "Model Choice" }]);
+let nextId = 0;
+let panels = $state<Panel[]>([{ id: nextId++, selected: "Model Choice" }]);
- const viewOptions = ["Select a view", "Model Choice", "Key Usage", "Claude Reset", "Model Status", "Tasks", "Config", "Skills", "Tools", "System Prompt", "Settings"];
+const viewOptions = [
+ "Select a view",
+ "Model Choice",
+ "Key Usage",
+ "Claude Reset",
+ "Model Status",
+ "Tasks",
+ "Config",
+ "Skills",
+ "Tools",
+ "System Prompt",
+ "Settings",
+];
- function addPanel() {
- panels = [...panels, { id: nextId++, selected: "Select a view" }];
- }
+function addPanel() {
+ panels = [...panels, { id: nextId++, selected: "Select a view" }];
+}
- function panelClass(selected: string): string {
- const base = "bg-base-200 rounded-lg p-3 flex flex-col min-h-0";
- const fill = selected === "Key Usage" || selected === "Claude Reset" || selected === "Tasks";
- return fill ? base + " flex-1" : base;
- }
+function panelClass(selected: string): string {
+ const base = "bg-base-200 rounded-lg p-3 flex flex-col min-h-0";
+ const fill = selected === "Key Usage" || selected === "Claude Reset" || selected === "Tasks";
+ return fill ? base + " flex-1" : base;
+}
- function contentClass(selected: string): string {
- const fill = selected === "Key Usage" || selected === "Claude Reset" || selected === "Tasks";
- return fill ? "mt-2 flex-1 min-h-0" : "mt-2";
- }
+function contentClass(selected: string): string {
+ const fill = selected === "Key Usage" || selected === "Claude Reset" || selected === "Tasks";
+ return fill ? "mt-2 flex-1 min-h-0" : "mt-2";
+}
</script>
<div class="flex flex-col gap-2">
@@ -115,7 +127,7 @@
{:else if panel.selected === "Skills"}
<SkillsBrowser {apiBase} />
{:else if panel.selected === "Tools"}
- <PermissionLog entries={permissionLog} {apiBase} />
+ <ToolPermissions entries={permissionLog} {apiBase} />
{:else if panel.selected === "System Prompt"}
<SystemPromptPanel {apiBase} />
{: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 @@
<script lang="ts">
- interface TaskItem {
- id: string;
- title: string;
- description: string;
- status: "pending" | "in_progress" | "done" | "blocked";
- }
+interface TaskItem {
+ id: string;
+ title: string;
+ description: string;
+ status: "pending" | "in_progress" | "done";
+}
- const { tasks }: { tasks: TaskItem[] } = $props();
+const { tasks }: { tasks: TaskItem[] } = $props();
- const doneCount = $derived(tasks.filter((t) => t.status === "done").length);
- const inProgressCount = $derived(tasks.filter((t) => t.status === "in_progress").length);
+const doneCount = $derived(tasks.filter((t) => t.status === "done").length);
+const inProgressCount = $derived(tasks.filter((t) => t.status === "in_progress").length);
- function badgeClass(status: TaskItem["status"]): string {
- switch (status) {
- case "pending":
- return "badge badge-ghost badge-xs";
- case "in_progress":
- return "badge badge-info badge-xs";
- case "done":
- return "badge badge-success badge-xs";
- case "blocked":
- return "badge badge-warning badge-xs";
- }
+function checkboxClass(status: TaskItem["status"]): string {
+ switch (status) {
+ case "pending":
+ return "checkbox checkbox-sm rounded-sm checkbox-secondary";
+ case "in_progress":
+ return "checkbox checkbox-sm rounded-sm checkbox-info";
+ case "done":
+ return "checkbox checkbox-sm rounded-sm checkbox-success";
}
+}
+
+function isChecked(status: TaskItem["status"]): boolean {
+ return status === "done";
+}
- function statusIcon(status: TaskItem["status"]): string {
- switch (status) {
- case "pending":
- return "⏳";
- case "in_progress":
- return "▶";
- case "done":
- return "✓";
- case "blocked":
- return "⚠";
- }
+function isIndeterminate(status: TaskItem["status"]): boolean {
+ return status === "in_progress";
+}
+
+function statusLabel(status: TaskItem["status"]): string {
+ switch (status) {
+ case "pending":
+ return "Pending";
+ case "in_progress":
+ return "In progress";
+ case "done":
+ return "Done";
}
+}
</script>
<div class="flex flex-col gap-2">
@@ -43,28 +47,35 @@
<p class="text-xs text-base-content/50">No tasks yet.</p>
{:else}
<p class="text-xs text-base-content/60">
- {tasks.length} task{tasks.length !== 1 ? "s" : ""}
- ({doneCount} done, {inProgressCount} in progress)
+ {doneCount}/{tasks.length} done{#if inProgressCount > 0}, {inProgressCount} in progress{/if}
</p>
- <ul class="flex flex-col gap-1">
+ <ul class="flex flex-col gap-0.5">
{#each tasks as task (task.id)}
- <li class="flex flex-col gap-0.5 rounded p-1.5 hover:bg-base-200 transition-colors">
- <div class="flex items-center gap-1.5">
- <span class={badgeClass(task.status)}>
- {statusIcon(task.status)}
- </span>
+ <li
+ class="flex items-start gap-2 rounded p-1.5 transition-colors {task.status === 'done' ? 'opacity-60' : ''}"
+ >
+ <input
+ type="checkbox"
+ class={checkboxClass(task.status)}
+ checked={isChecked(task.status)}
+ indeterminate={isIndeterminate(task.status)}
+ disabled
+ tabindex="-1"
+ />
+ <div class="flex flex-col gap-0.5 min-w-0">
<span
- class="text-sm leading-tight {task.status === 'in_progress'
- ? 'font-bold'
- : 'font-medium'}"
+ class="text-xs leading-tight {task.status === 'done'
+ ? 'line-through text-base-content/50'
+ : task.status === 'in_progress'
+ ? 'font-semibold'
+ : ''}"
>
{task.title}
</span>
+ {#if task.description}
+ <p class="text-xs text-base-content/50 line-clamp-2">{task.description}</p>
+ {/if}
</div>
- {#if task.description}
- <p class="text-xs text-base-content/60 line-clamp-2 pl-5">{task.description}</p>
- {/if}
- <p class="text-xs text-base-content/30 pl-5 font-mono">{task.id}</p>
</li>
{/each}
</ul>
diff --git a/packages/frontend/src/lib/components/PermissionLog.svelte b/packages/frontend/src/lib/components/ToolPermissions.svelte
index 7733dcf..c6af47f 100644
--- a/packages/frontend/src/lib/components/PermissionLog.svelte
+++ b/packages/frontend/src/lib/components/ToolPermissions.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
-import type { LogEntry } from "../types.js";
import { appSettings } from "../settings.svelte.js";
+import type { LogEntry } from "../types.js";
const { entries, apiBase = "" }: { entries: LogEntry[]; apiBase?: string } = $props();
@@ -13,9 +13,22 @@ interface ToolPermission {
const toolPermissions: ToolPermission[] = [
{ id: "read", label: "Read files", description: "Allow the AI to read files in the workspace" },
- { id: "edit", label: "Edit files", description: "Allow the AI to write/edit files in the workspace" },
+ {
+ id: "edit",
+ label: "Edit files",
+ description: "Allow the AI to write/edit files in the workspace",
+ },
{ id: "bash", label: "Run commands", description: "Allow the AI to execute shell commands" },
- { id: "external_directory", label: "External directories", description: "Allow access to files outside the workspace" },
+ {
+ id: "summon",
+ label: "Summon agents",
+ description: "Allow the AI to spawn child agents to work on tasks",
+ },
+ {
+ id: "external_directory",
+ label: "External directories",
+ description: "Allow access to files outside the workspace",
+ },
];
async function loadPermissions(): Promise<void> {
@@ -24,7 +37,7 @@ async function loadPermissions(): Promise<void> {
try {
const res = await fetch(`${apiBase}/tabs/settings/perm_${perm.id}`);
if (res.ok) {
- const data = await res.json() as { value: string | null };
+ const data = (await res.json()) as { value: string | null };
if (data.value !== null) {
loaded[perm.id] = data.value === "allow";
}