summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-23 05:25:03 +0900
committerAdam Malczewski <[email protected]>2026-05-23 05:25:03 +0900
commit225d3ea65cfc35d211fc66e30cf05cbc693d37e4 (patch)
tree1d765743942eb8c5d519fea37763ed6a36b90eec
parent9287cccb29d135ea19f2612c26f3090c94820d8c (diff)
downloaddispatch-225d3ea65cfc35d211fc66e30cf05cbc693d37e4.tar.gz
dispatch-225d3ea65cfc35d211fc66e30cf05cbc693d37e4.zip
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
-rw-r--r--packages/api/src/agent-manager.ts17
-rw-r--r--packages/api/src/routes/agents.ts9
-rw-r--r--packages/core/src/types/index.ts1
-rw-r--r--packages/frontend/src/lib/components/AgentBuilder.svelte12
-rw-r--r--packages/frontend/src/lib/tabs.svelte.ts3
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<boolean | null>(null);
+ let cwdResolved = $state<string | null>(null);
let cwdCheckTimer: ReturnType<typeof setTimeout> | null = null;
let formSkills = $state<Set<string>>(new Set());
let formTools = $state<Set<string>>(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();
</div>
{#if cwdExists === false && formCwd.trim()}
<span class="text-xs text-warning">Directory will be created automatically on first message.</span>
- {:else}
- <span class="text-xs text-base-content/50">Absolute path. Leave empty to use the project root.</span>
+ {/if}
+ {#if cwdResolved && formCwd.trim() && cwdResolved !== formCwd.trim()}
+ <span class="text-xs text-base-content/50 font-mono">{cwdResolved}</span>
+ {/if}
+ {#if !formCwd.trim()}
+ <span class="text-xs text-base-content/50">Absolute or relative path. Relative paths resolve against the parent agent's working directory, or the project root.</span>
{/if}
</div>
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];