9.1 KiB
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Workspace profile support | Add full workspace profile and custom path support to the Ironclaw web app and the dench SKILL.md, so they respect OPENCLAW_PROFILE, OPENCLAW_HOME, OPENCLAW_STATE_DIR, and per-agent workspace config — matching the CLI's existing resolution logic. |
|
false |
Full Workspace Profile and Custom Path Support
Problem
The CLI core (src/agents/workspace.ts, src/config/paths.ts) already resolves workspace paths dynamically via OPENCLAW_PROFILE, OPENCLAW_HOME, OPENCLAW_STATE_DIR, and per-agent config — but the web app (apps/web/) and the injected dench skill (skills/dench/SKILL.md) hardcode ~/.openclaw and ~/.openclaw/workspace everywhere, ignoring profiles entirely.
35 hardcoded ~/.openclaw references in SKILL.md, and ~15 hardcoded paths across the web app API routes and UI.
Approach
1. Centralize path resolution in the web app
Create two new helpers in apps/web/lib/workspace.ts and update the existing resolveWorkspaceRoot():
**resolveOpenClawStateDir()**— mirrorssrc/config/paths.ts:resolveStateDir()logic: checksOPENCLAW_STATE_DIRenv var, thenOPENCLAW_HOME, falls back to~/.openclaw. Returns the base state directory (e.g.~/.openclaw).- **Update
resolveWorkspaceRoot()**— addOPENCLAW_PROFILEawareness between theOPENCLAW_WORKSPACEcheck and the fallback:OPENCLAW_WORKSPACEenv var (existing)OPENCLAW_PROFILE-><stateDir>/workspace-<profile>(new)<stateDir>/workspace(existing, but now usesresolveOpenClawStateDir()instead of hardcoded~/.openclaw)
export function resolveOpenClawStateDir(): string {
const override = process.env.OPENCLAW_STATE_DIR?.trim();
if (override) return override.startsWith("~") ? join(homedir(), override.slice(1)) : override;
const home = process.env.OPENCLAW_HOME?.trim() || homedir();
return join(home, ".openclaw");
}
export function resolveWorkspaceRoot(): string | null {
const stateDir = resolveOpenClawStateDir();
const profile = process.env.OPENCLAW_PROFILE?.trim();
const candidates = [
process.env.OPENCLAW_WORKSPACE,
profile && profile.toLowerCase() !== "default" ? join(stateDir, `workspace-${profile}`) : null,
join(stateDir, "workspace"),
].filter(Boolean) as string[];
for (const dir of candidates) {
if (existsSync(dir)) return dir;
}
return null;
}
2. Replace all hardcoded paths in web app API routes
Every file below uses join(homedir(), ".openclaw", ...) directly. Replace with calls to resolveOpenClawStateDir() or resolveWorkspaceRoot():
| File | What to change |
|---|---|
apps/web/app/api/workspace/tree/route.ts |
join(home, ".openclaw", "skills") and join(home, ".openclaw") -> resolveOpenClawStateDir() |
apps/web/app/api/workspace/virtual-file/route.ts |
All 6 hardcoded paths in resolveVirtualPath() and isSafePath() -> derive from resolveWorkspaceRoot() and resolveOpenClawStateDir() |
apps/web/app/api/skills/route.ts |
join(openclawDir, "skills") and join(openclawDir, "workspace", "skills") -> use both helpers |
apps/web/app/api/sessions/route.ts |
resolveOpenClawDir() local helper -> use shared resolveOpenClawStateDir() |
apps/web/app/api/memories/route.ts |
join(homedir(), ".openclaw", "workspace") -> resolveWorkspaceRoot() |
apps/web/app/api/cron/jobs/route.ts |
Module-level CRON_DIR and agentsDir -> derive from resolveOpenClawStateDir() |
apps/web/app/api/cron/runs/search-transcript/route.ts |
agents dir -> resolveOpenClawStateDir() |
apps/web/app/api/cron/runs/[sessionId]/route.ts |
agents dir -> resolveOpenClawStateDir() |
apps/web/app/api/cron/jobs/[jobId]/runs/route.ts |
if hardcoded -> resolveOpenClawStateDir() |
apps/web/app/api/web-sessions/route.ts |
WEB_CHAT_DIR -> derive from resolveOpenClawStateDir() |
apps/web/app/api/web-sessions/[id]/route.ts |
same |
apps/web/app/api/web-sessions/[id]/messages/route.ts |
same |
apps/web/lib/active-runs.ts |
WEB_CHAT_DIR -> derive from resolveOpenClawStateDir() |
3. Update the UI empty state
In apps/web/app/components/workspace/empty-state.tsx (line 128): the hardcoded ~/.openclaw/workspace display string should be dynamic. Two options:
- Option A: Pass the resolved workspace path from the tree API response (it already returns
workspaceRoot). The empty state can show that or a user-friendly tilde-collapsed version. - Option B: Add an API endpoint or server component that returns the expected workspace path.
Option A is simplest — the tree API already returns openclawDir and workspaceRoot. Thread the expected path into the empty state component.
4. Fix hardcoded path in system prompt
In src/agents/system-prompt.ts line 173: the hardcoded ~/.openclaw/web-chat/ should use the workspaceDir parameter (or derive from the state dir that's already available in the prompt builder context). Replace with a template string that references the actual state directory.
5. Add workspace variable substitution for injected SKILL.md content
The dench SKILL.md has 35 instances of ~/.openclaw/workspace. Since this content is injected verbatim into the system prompt via readSkillContent(), we need a substitution mechanism.
In src/agents/skills/workspace.ts around line 271 where readSkillContent() is called for injected skills:
// After reading content, substitute workspace path placeholders
const content = readSkillContent(entry.skill.filePath);
if (content) {
const resolved = content.replaceAll("~/.openclaw/workspace", workspaceDir);
injectedSkills.push({ name: entry.skill.name, content: resolved });
}
This requires threading workspaceDir into buildWorkspaceSkillSnapshot() — which it already receives as its first argument.
Then update skills/dench/SKILL.md to use ~/.openclaw/workspace as a canonical placeholder (it already does), and the substitution will replace it with the actual resolved path at injection time. No changes needed to the SKILL.md content itself.
6. Expose workspace info in the tree API response
The tree API (apps/web/app/api/workspace/tree/route.ts) already returns workspaceRoot and openclawDir. Consider also returning profile (from OPENCLAW_PROFILE) so the UI can display profile-aware context (e.g. "Workspace (staging)" in the sidebar).