From 225d3ea65cfc35d211fc66e30cf05cbc693d37e4 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 23 May 2026 05:25:03 +0900 Subject: feat: relative working directory support and subagent tab cwd propagation - Resolve relative cwd paths (e.g. ./subtask) against parent's working directory at runtime - check-dir endpoint resolves relative paths and returns the resolved absolute path - AgentBuilder shows resolved path below input for relative paths, updated helper text - tab-created event now includes workingDirectory so subagent tabs display their cwd in sidebar - Add workingDirectory to tab-created AgentEvent type definition - spawnChildAgent stores resolved absolute path instead of raw relative path --- packages/api/src/agent-manager.ts | 17 ++++++++++++++++- packages/api/src/routes/agents.ts | 9 +++++++-- packages/core/src/types/index.ts | 1 + .../frontend/src/lib/components/AgentBuilder.svelte | 12 ++++++++++-- packages/frontend/src/lib/tabs.svelte.ts | 3 ++- 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts index 74995b2..d473950 100644 --- a/packages/api/src/agent-manager.ts +++ b/packages/api/src/agent-manager.ts @@ -329,6 +329,15 @@ export class AgentManager { workingDirectory = join(homedir(), workingDirectory.slice(1)); } + // Resolve relative paths against the default working directory + // (e.g. subagent cwd "./subtask" resolves relative to the parent's effective dir) + { + const { isAbsolute, resolve } = await import("node:path"); + if (!isAbsolute(workingDirectory)) { + workingDirectory = resolve(defaultWorkDir, workingDirectory); + } + } + // Auto-create the working directory if it doesn't exist try { const { mkdirSync, existsSync } = await import("node:fs"); @@ -668,6 +677,8 @@ export class AgentManager { parentEffectiveDir = join(homedir(), parentEffectiveDir.slice(1)); } + // Resolve and validate child working directory against parent's effective dir + let resolvedWorkingDirectory = options.workingDirectory; if (options.workingDirectory) { const { isAbsolute, relative, resolve, join } = await import("node:path"); // Expand ~ in child working directory @@ -685,6 +696,9 @@ export class AgentManager { `Working directory "${options.workingDirectory}" is outside the parent's working directory "${parentDir}".`, ); } + // Store the resolved absolute path so downstream code doesn't + // re-resolve against the wrong base directory + resolvedWorkingDirectory = resolved; } // Intersect requested tools with parent's allowed tools to prevent privilege escalation @@ -696,7 +710,7 @@ export class AgentManager { // Create the tab agent entry with overrides const tabAgent = this._getOrCreateTabAgent(tabId); tabAgent.toolsOverride = childTools; - tabAgent.workingDirectoryOverride = options.workingDirectory; + tabAgent.workingDirectoryOverride = resolvedWorkingDirectory; tabAgent.keyId = options.parentKeyId ?? null; tabAgent.modelId = options.parentModelId ?? null; tabAgent.finalOutput = ""; @@ -727,6 +741,7 @@ export class AgentManager { keyId: tabAgent.keyId, modelId: tabAgent.modelId, parentTabId: options.parentTabId ?? null, + workingDirectory: resolvedWorkingDirectory ?? null, }, tabId, ); diff --git a/packages/api/src/routes/agents.ts b/packages/api/src/routes/agents.ts index d3ca23f..1627674 100644 --- a/packages/api/src/routes/agents.ts +++ b/packages/api/src/routes/agents.ts @@ -99,11 +99,16 @@ agentsRoutes.get("/check-dir", (c) => { if (dirPath === "~" || dirPath.startsWith("~/")) { dirPath = path.join(os.homedir(), dirPath.slice(1)); } + // Resolve relative paths against the project root + if (!path.isAbsolute(dirPath)) { + const projectDir = process.env.DISPATCH_WORKING_DIR || process.cwd(); + dirPath = path.resolve(projectDir, dirPath); + } try { const stat = fs.statSync(dirPath); - return c.json({ exists: stat.isDirectory() }); + return c.json({ exists: stat.isDirectory(), resolved: dirPath }); } catch { - return c.json({ exists: false }); + return c.json({ exists: false, resolved: dirPath }); } }); diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index ff47f49..c301822 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -47,6 +47,7 @@ export type AgentEvent = keyId: string | null; modelId: string | null; parentTabId: string | null; + workingDirectory: string | null; } | { type: "message-queued"; tabId: string; messageId: string; message: string } | { type: "message-consumed"; tabId: string; messageIds: string[] } diff --git a/packages/frontend/src/lib/components/AgentBuilder.svelte b/packages/frontend/src/lib/components/AgentBuilder.svelte index 65fd764..d85a6a3 100644 --- a/packages/frontend/src/lib/components/AgentBuilder.svelte +++ b/packages/frontend/src/lib/components/AgentBuilder.svelte @@ -62,6 +62,7 @@ const modelCache = new Map(); let formScope = $state("global"); let formCwd = $state(""); let cwdExists = $state(null); + let cwdResolved = $state(null); let cwdCheckTimer: ReturnType | null = null; let formSkills = $state>(new Set()); let formTools = $state>(new Set()); @@ -301,6 +302,7 @@ const modelCache = new Map(); const cwd = formCwd; if (!cwd.trim()) { cwdExists = null; + cwdResolved = null; return; } if (cwdCheckTimer) clearTimeout(cwdCheckTimer); @@ -312,9 +314,11 @@ const modelCache = new Map(); if (res.ok) { const data = await res.json(); cwdExists = data.exists ?? false; + cwdResolved = data.resolved ?? null; } } catch { cwdExists = null; + cwdResolved = null; } }, 300); }); @@ -479,8 +483,12 @@ const modelCache = new Map(); {#if cwdExists === false && formCwd.trim()} Directory will be created automatically on first message. - {:else} - Absolute path. Leave empty to use the project root. + {/if} + {#if cwdResolved && formCwd.trim() && cwdResolved !== formCwd.trim()} + {cwdResolved} + {/if} + {#if !formCwd.trim()} + Absolute or relative path. Relative paths resolve against the parent agent's working directory, or the project root. {/if} diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts index 7f4d768..c488a92 100644 --- a/packages/frontend/src/lib/tabs.svelte.ts +++ b/packages/frontend/src/lib/tabs.svelte.ts @@ -462,6 +462,7 @@ function createTabStore() { keyId: string | null; modelId: string | null; parentTabId: string | null; + workingDirectory: string | null; }; // Only add if we don't already have this tab if (!getTabById(newTabEvent.id)) { @@ -480,7 +481,7 @@ function createTabStore() { persistent: newTabEvent.parentTabId == null, agentSlug: null, agentScope: null, - workingDirectory: null, + workingDirectory: newTabEvent.workingDirectory ?? null, queuedMessages: [], }; tabs = [...tabs, tab]; -- cgit v1.2.3