summaryrefslogtreecommitdiffhomepage
path: root/packages/api/src/agent-manager.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 15:24:13 +0900
committerAdam Malczewski <[email protected]>2026-05-22 15:24:13 +0900
commit9ecaabd87c0e51b8a7408dabb0133a9344586859 (patch)
tree4b5809ab23948a5e3f558f3aa34c52d5038f4e19 /packages/api/src/agent-manager.ts
parent8f1dd855f0c4c877bff8d3a4ba193432b268b1c2 (diff)
downloaddispatch-9ecaabd87c0e51b8a7408dabb0133a9344586859.tar.gz
dispatch-9ecaabd87c0e51b8a7408dabb0133a9344586859.zip
feat: agent builder, CWD support, auto-save, UI polish, unavailable tool handling
- Agent Builder: full CRUD with card grid, drag-and-drop model reorder, edit/delete - Auto-save on edit with 600ms debounce, AbortController for concurrency, fieldset disabled until name entered - Agent definitions stored as TOML with cwd field, loaded from global/project dirs - Working directory: per-tab CWD override in Chat Settings, agent default CWD, auto-create on first message - CWD validation: check-dir endpoint with ~ expansion, real-time validity indicator - Subagent CWD validated against parent's effective CWD using path.relative - Unavailable tool calls: caught gracefully, shown as tool call with error badge, model retries - UI: tab bar border radius, sidebar border removed, chat input ghost style, scroll-to-bottom rectangle - Skills dir collapse uses CSS rotation, Model Choice renamed to Chat Settings, System Prompt view removed - Reusable SkillsBrowser/ToolPermissions with external mode for Agent Builder - ModelSelector: Agent/Manual toggle, agent list, Agent Settings link - Page router, skills recursive scanning, bin/up gopass removed, docker volume mounts
Diffstat (limited to 'packages/api/src/agent-manager.ts')
-rw-r--r--packages/api/src/agent-manager.ts61
1 files changed, 54 insertions, 7 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
index 7e814af..82b8456 100644
--- a/packages/api/src/agent-manager.ts
+++ b/packages/api/src/agent-manager.ts
@@ -299,7 +299,24 @@ export class AgentManager {
if (!tabAgent.agent) {
const defaultWorkDir = process.env.DISPATCH_WORKING_DIR ?? process.cwd();
- const workingDirectory = tabAgent.workingDirectoryOverride ?? defaultWorkDir;
+ let workingDirectory = tabAgent.workingDirectoryOverride ?? defaultWorkDir;
+
+ // Expand ~ to home directory
+ if (workingDirectory === "~" || workingDirectory.startsWith("~/")) {
+ const { homedir } = await import("node:os");
+ const { join } = await import("node:path");
+ workingDirectory = join(homedir(), workingDirectory.slice(1));
+ }
+
+ // Auto-create the working directory if it doesn't exist
+ try {
+ const { mkdirSync, existsSync } = await import("node:fs");
+ if (!existsSync(workingDirectory)) {
+ mkdirSync(workingDirectory, { recursive: true });
+ }
+ } catch {
+ // Ignore — tool execution will surface the error naturally
+ }
// Build tools list — child agents use their toolsOverride whitelist,
// parent agents use permission settings from DB
@@ -573,15 +590,34 @@ export class AgentManager {
const tabId = crypto.randomUUID();
const title = options.task.length > 50 ? `${options.task.slice(0, 47)}...` : options.task;
- // Validate working directory is within the parent's workspace
+ // Validate working directory is within the parent agent's effective CWD
const defaultWorkDir = process.env.DISPATCH_WORKING_DIR ?? process.cwd();
+ let parentEffectiveDir = options.parentTabId
+ ? (this.tabAgents.get(options.parentTabId)?.workingDirectoryOverride ?? defaultWorkDir)
+ : defaultWorkDir;
+
+ // Expand ~ in parent dir
+ if (parentEffectiveDir === "~" || parentEffectiveDir.startsWith("~/")) {
+ const { homedir } = await import("node:os");
+ const { join } = await import("node:path");
+ parentEffectiveDir = join(homedir(), parentEffectiveDir.slice(1));
+ }
+
if (options.workingDirectory) {
- const { resolve } = await import("node:path");
- const resolved = resolve(options.workingDirectory);
- const parentDir = resolve(defaultWorkDir);
- if (!resolved.startsWith(`${parentDir}/`) && resolved !== parentDir) {
+ const { isAbsolute, relative, resolve, join } = await import("node:path");
+ // Expand ~ in child working directory
+ let childDir = options.workingDirectory;
+ if (childDir === "~" || childDir.startsWith("~/")) {
+ const { homedir } = await import("node:os");
+ childDir = join(homedir(), childDir.slice(1));
+ }
+ const parentDir = resolve(parentEffectiveDir);
+ const resolved = resolve(parentDir, childDir);
+ const rel = relative(parentDir, resolved);
+ const isOutside = rel.startsWith("..") || isAbsolute(rel);
+ if (isOutside) {
throw new Error(
- `Working directory "${options.workingDirectory}" is outside the workspace "${parentDir}".`,
+ `Working directory "${options.workingDirectory}" is outside the parent's working directory "${parentDir}".`,
);
}
}
@@ -676,8 +712,19 @@ export class AgentManager {
keyId?: string,
modelId?: string,
reasoningEffort?: "none" | "low" | "medium" | "high" | "max",
+ workingDirectory?: string,
): Promise<void> {
const tabAgent = this._getOrCreateTabAgent(tabId);
+
+ // Apply working directory override from frontend if provided
+ if (workingDirectory !== undefined) {
+ const prevDir = tabAgent.workingDirectoryOverride;
+ tabAgent.workingDirectoryOverride = workingDirectory || undefined;
+ // Invalidate cached agent if working directory changed
+ if (prevDir !== tabAgent.workingDirectoryOverride) {
+ tabAgent.agent = null;
+ }
+ }
tabAgent.abortController = new AbortController();
tabAgent.status = "running";
this.messageCount += 1;