summaryrefslogtreecommitdiffhomepage
path: root/packages/api/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-23 05:06:12 +0900
committerAdam Malczewski <[email protected]>2026-05-23 05:06:12 +0900
commit9287cccb29d135ea19f2612c26f3090c94820d8c (patch)
tree2d68e8cacf6d71786f305d5f4a512a68f19137c5 /packages/api/src
parentef427d3eae77fca716c203dd8bd84939710c518a (diff)
downloaddispatch-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.ts83
-rw-r--r--packages/api/src/app.ts7
-rw-r--r--packages/api/src/routes/agents.ts1
-rw-r--r--packages/api/src/routes/models.ts12
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");