diff options
| author | Adam Malczewski <[email protected]> | 2026-05-23 05:06:12 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-23 05:06:12 +0900 |
| commit | 9287cccb29d135ea19f2612c26f3090c94820d8c (patch) | |
| tree | 2d68e8cacf6d71786f305d5f4a512a68f19137c5 /packages/api/src | |
| parent | ef427d3eae77fca716c203dd8bd84939710c518a (diff) | |
| download | dispatch-9287cccb29d135ea19f2612c26f3090c94820d8c.tar.gz dispatch-9287cccb29d135ea19f2612c26f3090c94820d8c.zip | |
feat: add is_subagent flag to agents, fix all lint/type/test issues
- Add is_subagent checkbox to agent editor; subagents are hidden from Chat Settings
- Add is_subagent field to AgentDefinition type, TOML serialization, and API route
- Filter subagents from ModelSelector agent list
- Fix all biome lint/format errors across codebase (useLiteralKeys, noNonNullAssertion, noExplicitAny, formatting, import sorting)
- Fix svelte-check errors (type narrowing in SkillsBrowser, ToolPermissions, SidebarPanel)
- Fix a11y warnings in App.svelte (label-control associations)
- Fix test mocks missing BackgroundShellStore, BackgroundTranscriptStore, createWebSearchTool, createYoutubeTranscribeTool
- Update stale 409 test to match current message-queuing behavior
- Exclude packaging/ and release/ dirs from biome to avoid linting stale build artifacts
Diffstat (limited to 'packages/api/src')
| -rw-r--r-- | packages/api/src/agent-manager.ts | 83 | ||||
| -rw-r--r-- | packages/api/src/app.ts | 7 | ||||
| -rw-r--r-- | packages/api/src/routes/agents.ts | 1 | ||||
| -rw-r--r-- | packages/api/src/routes/models.ts | 12 |
4 files changed, 60 insertions, 43 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts index 3789b68..74995b2 100644 --- a/packages/api/src/agent-manager.ts +++ b/packages/api/src/agent-manager.ts @@ -4,6 +4,8 @@ import { type AgentSkillMapping, type AgentStatus, appendMessage, + BackgroundShellStore, + BackgroundTranscriptStore, type ClaudeAccount, configToRuleset, createConfigWatcher, @@ -11,8 +13,6 @@ import { createReadFileTool, createRetrieveTool, createRunShellTool, - BackgroundShellStore, - BackgroundTranscriptStore, createSkillsWatcher, createSummonTool, createTaskListTool, @@ -298,13 +298,15 @@ export class AgentManager { const effectiveKeyId = keyId ?? tabAgent.keyId; const effectiveModelId = modelId ?? tabAgent.modelId; - // Read tool permission settings from DB (default: read=allow, edit=ask, bash=ask, summon=ask) + // Read tool permission settings from DB (default: read=allow, edit=ask, bash=ask, summon=ask, web=ask, youtube=ask) const permRead = getSetting("perm_read") !== "ask"; const permEdit = getSetting("perm_edit") === "allow"; const permBash = getSetting("perm_bash") === "allow"; const permSummon = getSetting("perm_summon") === "allow"; + const permWebSearch = getSetting("perm_web_search") === "allow"; + const permYoutubeTranscribe = getSetting("perm_youtube_transcribe") === "allow"; const sysPrompt = getSetting("system_prompt") ?? ""; - const permKey = `${permRead}:${permEdit}:${permBash}:${permSummon}:${sysPrompt}`; + const permKey = `${permRead}:${permEdit}:${permBash}:${permSummon}:${permWebSearch}:${permYoutubeTranscribe}:${sysPrompt}`; // If the override differs or permissions changed, invalidate the cached agent if ( @@ -358,13 +360,19 @@ export class AgentManager { toolEntries.push({ name: "write_file", tool: createWriteFileTool(workingDirectory) }); } if (allowed.has("run_shell")) { - toolEntries.push({ name: "run_shell", tool: createRunShellTool(workingDirectory, tabAgent.shellStore) }); + toolEntries.push({ + name: "run_shell", + tool: createRunShellTool(workingDirectory, tabAgent.shellStore), + }); } if (allowed.has("web_search")) { toolEntries.push({ name: "web_search", tool: createWebSearchTool() }); } if (allowed.has("youtube_transcribe")) { - toolEntries.push({ name: "youtube_transcribe", tool: createYoutubeTranscribeTool(tabAgent.transcriptStore) }); + toolEntries.push({ + name: "youtube_transcribe", + tool: createYoutubeTranscribeTool(tabAgent.transcriptStore), + }); } if (allowed.has("todo")) { toolEntries.push({ name: "todo", tool: createTaskListTool(tabAgent.taskList) }); @@ -408,10 +416,20 @@ export class AgentManager { toolEntries.push({ name: "write_file", tool: createWriteFileTool(workingDirectory) }); } if (permBash) { - toolEntries.push({ name: "run_shell", tool: createRunShellTool(workingDirectory, tabAgent.shellStore) }); + toolEntries.push({ + name: "run_shell", + tool: createRunShellTool(workingDirectory, tabAgent.shellStore), + }); + } + if (permWebSearch) { + toolEntries.push({ name: "web_search", tool: createWebSearchTool() }); + } + if (permYoutubeTranscribe) { + toolEntries.push({ + name: "youtube_transcribe", + tool: createYoutubeTranscribeTool(tabAgent.transcriptStore), + }); } - toolEntries.push({ name: "web_search", tool: createWebSearchTool() }); - toolEntries.push({ name: "youtube_transcribe", tool: createYoutubeTranscribeTool(tabAgent.transcriptStore) }); toolEntries.push({ name: "todo", tool: createTaskListTool(tabAgent.taskList) }); if (permSummon) { // Capture parent's allowed tool names for child permission enforcement @@ -544,25 +562,25 @@ export class AgentManager { tabAgent.modelId = null; } - const customSystemPrompt = getSetting("system_prompt") || undefined; - tabAgent.agent = new Agent( - { - model, - apiKey, - baseURL, - systemPrompt: buildSystemPrompt(toolNames, customSystemPrompt), - tools, - workingDirectory, - permissionChecker: this.permissionManager ?? undefined, - ruleset, - provider, - ...(claudeCredentials ? { claudeCredentials } : {}), - }, - { - dequeueMessages: () => this.dequeueMessages(tabId), - waitForQueuedMessage: () => this.waitForQueuedMessage(tabId), - }, - ); + const customSystemPrompt = getSetting("system_prompt") || undefined; + tabAgent.agent = new Agent( + { + model, + apiKey, + baseURL, + systemPrompt: buildSystemPrompt(toolNames, customSystemPrompt), + tools, + workingDirectory, + permissionChecker: this.permissionManager ?? undefined, + ruleset, + provider, + ...(claudeCredentials ? { claudeCredentials } : {}), + }, + { + dequeueMessages: () => this.dequeueMessages(tabId), + waitForQueuedMessage: () => this.waitForQueuedMessage(tabId), + }, + ); } return tabAgent.agent; } @@ -672,7 +690,7 @@ export class AgentManager { // Intersect requested tools with parent's allowed tools to prevent privilege escalation let childTools = options.tools; if (options.parentAllowedTools) { - childTools = options.tools.filter((t) => options.parentAllowedTools!.has(t)); + childTools = options.tools.filter((t) => options.parentAllowedTools?.has(t)); } // Create the tab agent entry with overrides @@ -931,10 +949,7 @@ export class AgentManager { const messages = [...tabAgent.messageQueue]; tabAgent.messageQueue = []; if (messages.length > 0) { - this.emit( - { type: "message-consumed", tabId, messageIds: messages.map((m) => m.id) }, - tabId, - ); + this.emit({ type: "message-consumed", tabId, messageIds: messages.map((m) => m.id) }, tabId); } return messages; } @@ -951,7 +966,7 @@ export class AgentManager { }); const cancel = () => { if (listener) { - tabAgent.queueListeners = tabAgent.queueListeners.filter(l => l !== listener); + tabAgent.queueListeners = tabAgent.queueListeners.filter((l) => l !== listener); listener = null; } }; diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index 53cfb6c..9ccc911 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -63,7 +63,8 @@ app.post("/chat", async (c) => { const keyId = typeof body.keyId === "string" ? body.keyId : undefined; const modelId = typeof body.modelId === "string" ? body.modelId : undefined; - const workingDirectory = typeof body.workingDirectory === "string" ? body.workingDirectory : undefined; + const workingDirectory = + typeof body.workingDirectory === "string" ? body.workingDirectory : undefined; const validEfforts = ["none", "low", "medium", "high", "max"]; const reasoningEffort = typeof body.reasoningEffort === "string" && validEfforts.includes(body.reasoningEffort) @@ -71,7 +72,9 @@ app.post("/chat", async (c) => { : undefined; // Non-blocking — let the agent run in the background - agentManager.processMessage(tabId, message, keyId, modelId, reasoningEffort, workingDirectory).catch(console.error); + agentManager + .processMessage(tabId, message, keyId, modelId, reasoningEffort, workingDirectory) + .catch(console.error); return c.json({ status: "ok" }); }); diff --git a/packages/api/src/routes/agents.ts b/packages/api/src/routes/agents.ts index 42339bf..d3ca23f 100644 --- a/packages/api/src/routes/agents.ts +++ b/packages/api/src/routes/agents.ts @@ -56,6 +56,7 @@ agentsRoutes.post("/", async (c) => { scope: body.scope, slug: body.slug, ...(body.cwd ? { cwd: body.cwd } : {}), + ...(body.is_subagent ? { is_subagent: true } : {}), }; saveAgent(agent); return c.json({ ok: true, agent }); diff --git a/packages/api/src/routes/models.ts b/packages/api/src/routes/models.ts index d6b82c2..b250263 100644 --- a/packages/api/src/routes/models.ts +++ b/packages/api/src/routes/models.ts @@ -112,7 +112,7 @@ modelsRoutes.get("/available", async (c) => { }); } - const apiKeyValue = resolveApiKey(keyId!); + const apiKeyValue = resolveApiKey(keyId); if (!apiKeyValue) { return c.json({ error: `no API key found for ${keyId}` }, 500); } @@ -420,10 +420,7 @@ modelsRoutes.post("/add-key", async (c) => { // Validate provider if (!VALID_PROVIDERS.includes(body.provider as SupportedProvider)) { - return c.json( - { error: `provider must be one of: ${VALID_PROVIDERS.join(", ")}` }, - 400, - ); + return c.json({ error: `provider must be one of: ${VALID_PROVIDERS.join(", ")}` }, 400); } const provider = body.provider as SupportedProvider; const base_url = PROVIDER_BASE_URLS[provider]; @@ -452,7 +449,7 @@ modelsRoutes.post("/add-key", async (c) => { newBlock += "\n"; // Insert before the # ─── Permissions section if it exists, otherwise at end - const permissionsMarker = /\n# [─\-]+ Permissions/; + const permissionsMarker = /\n# [─-]+ Permissions/; let newContent: string; const permMatch = permissionsMarker.exec(tomlContent); if (permMatch) { @@ -509,7 +506,8 @@ modelsRoutes.post("/remove-key", async (c) => { return c.json({ error: `key "${id}" not found in dispatch.toml` }, 404); } - const newContent = tomlContent.slice(0, match.index) + tomlContent.slice(match.index + match[0].length); + const newContent = + tomlContent.slice(0, match.index) + tomlContent.slice(match.index + match[0].length); try { writeFileSync(tomlPath, newContent, "utf-8"); |
