diff --git a/apps/web/app/api/workspace/tree-browse.test.ts b/apps/web/app/api/workspace/tree-browse.test.ts index e6a5e3c0e21..564aa88580a 100644 --- a/apps/web/app/api/workspace/tree-browse.test.ts +++ b/apps/web/app/api/workspace/tree-browse.test.ts @@ -17,8 +17,8 @@ vi.mock("node:os", () => ({ // Mock workspace vi.mock("@/lib/workspace", () => ({ resolveWorkspaceRoot: vi.fn(() => null), - resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw"), - getEffectiveProfile: vi.fn(() => "default"), + resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"), + getActiveWorkspaceName: vi.fn(() => null), parseSimpleYaml: vi.fn(() => ({})), duckdbQueryAll: vi.fn(() => []), duckdbQueryAllAsync: vi.fn(async () => []), @@ -57,8 +57,8 @@ describe("Workspace Tree & Browse API", () => { })); vi.mock("@/lib/workspace", () => ({ resolveWorkspaceRoot: vi.fn(() => null), - resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw"), - getEffectiveProfile: vi.fn(() => "default"), + resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"), + getActiveWorkspaceName: vi.fn(() => null), parseSimpleYaml: vi.fn(() => ({})), duckdbQueryAll: vi.fn(() => []), duckdbQueryAllAsync: vi.fn(async () => []), @@ -83,11 +83,13 @@ describe("Workspace Tree & Browse API", () => { const json = await res.json(); expect(json.exists).toBe(false); expect(json.tree).toEqual([]); + expect(json.workspace).toBeNull(); }); it("returns tree with workspace files", async () => { - const { resolveWorkspaceRoot } = await import("@/lib/workspace"); + const { resolveWorkspaceRoot, getActiveWorkspaceName } = await import("@/lib/workspace"); vi.mocked(resolveWorkspaceRoot).mockReturnValue("/ws"); + vi.mocked(getActiveWorkspaceName).mockReturnValue("default"); const { readdirSync: mockReaddir, existsSync: mockExists } = await import("node:fs"); vi.mocked(mockExists).mockReturnValue(true); vi.mocked(mockReaddir).mockImplementation((dir) => { @@ -106,6 +108,7 @@ describe("Workspace Tree & Browse API", () => { const json = await res.json(); expect(json.exists).toBe(true); expect(json.tree.length).toBeGreaterThan(0); + expect(json.workspace).toBe("default"); }); it("includes workspaceRoot in response", async () => { @@ -153,16 +156,16 @@ describe("Workspace Tree & Browse API", () => { const value = String(p); return ( value === "/ws" || - value === "/home/testuser/.openclaw/skills" || - value === "/home/testuser/.openclaw/skills/alpha/SKILL.md" || - value === "/home/testuser/.openclaw/skills/dench/SKILL.md" + value === "/ws/skills" || + value === "/ws/skills/alpha/SKILL.md" || + value === "/ws/skills/dench/SKILL.md" ); }); vi.mocked(mockReaddir).mockImplementation((dir) => { if (String(dir) === "/ws") { return [] as unknown as Dirent[]; } - if (String(dir) === "/home/testuser/.openclaw/skills") { + if (String(dir) === "/ws/skills") { return [ makeDirent("alpha", true), makeDirent("dench", true), diff --git a/apps/web/app/api/workspace/tree/route.ts b/apps/web/app/api/workspace/tree/route.ts index c28fdb3525b..3f0d640e984 100644 --- a/apps/web/app/api/workspace/tree/route.ts +++ b/apps/web/app/api/workspace/tree/route.ts @@ -1,6 +1,6 @@ import { readdirSync, readFileSync, existsSync, statSync, type Dirent } from "node:fs"; import { join } from "node:path"; -import { resolveWorkspaceRoot, resolveOpenClawStateDir, getEffectiveProfile, parseSimpleYaml, duckdbQueryAll, isDatabaseFile } from "@/lib/workspace"; +import { resolveWorkspaceRoot, resolveOpenClawStateDir, getActiveWorkspaceName, parseSimpleYaml, duckdbQueryAll, isDatabaseFile } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -186,12 +186,13 @@ function parseSkillFrontmatter(content: string): { name?: string; emoji?: string return { name: result.name, emoji: result.emoji }; } -/** Build a virtual "Skills" folder from /skills/. */ +/** Build a virtual "Skills" folder from /skills/. */ function buildSkillsVirtualFolder(): TreeNode | null { - const stateDir = resolveOpenClawStateDir(); - const dirs = [ - join(stateDir, "skills"), - ]; + const workspaceRoot = resolveWorkspaceRoot(); + if (!workspaceRoot) { + return null; + } + const dirs = [join(workspaceRoot, "skills")]; const children: TreeNode[] = []; const seen = new Set(); @@ -247,13 +248,13 @@ export async function GET(req: Request) { const showHidden = url.searchParams.get("showHidden") === "1"; const openclawDir = resolveOpenClawStateDir(); - const profile = getEffectiveProfile(); + const workspace = getActiveWorkspaceName(); const root = resolveWorkspaceRoot(); if (!root) { const tree: TreeNode[] = []; const skillsFolder = buildSkillsVirtualFolder(); if (skillsFolder) {tree.push(skillsFolder);} - return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir, profile }); + return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir, workspace }); } const dbObjects = loadDbObjects(); @@ -263,5 +264,5 @@ export async function GET(req: Request) { const skillsFolder = buildSkillsVirtualFolder(); if (skillsFolder) {tree.push(skillsFolder);} - return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir, profile }); + return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir, workspace }); } diff --git a/apps/web/app/api/workspace/virtual-file/route.ts b/apps/web/app/api/workspace/virtual-file/route.ts index 33b17f0ae6c..ed9ce32680a 100644 --- a/apps/web/app/api/workspace/virtual-file/route.ts +++ b/apps/web/app/api/workspace/virtual-file/route.ts @@ -1,6 +1,6 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; import { join, dirname, resolve, normalize } from "node:path"; -import { resolveOpenClawStateDir, resolveWorkspaceRoot } from "@/lib/workspace"; +import { resolveWorkspaceRoot } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -10,8 +10,10 @@ export const runtime = "nodejs"; * Returns null if the path is invalid or tries to escape. */ function resolveVirtualPath(virtualPath: string): string | null { - const stateDir = resolveOpenClawStateDir(); - const workspaceDir = resolveWorkspaceRoot() ?? join(stateDir, "workspace"); + const workspaceDir = resolveWorkspaceRoot(); + if (!workspaceDir) { + return null; + } if (virtualPath.startsWith("~skills/")) { // ~skills//SKILL.md @@ -27,18 +29,12 @@ function resolveVirtualPath(virtualPath: string): string | null { return null; } - // Check workspace skills first, then managed skills - const candidates = [ - join(workspaceDir, "skills", skillName, "SKILL.md"), - join(stateDir, "skills", skillName, "SKILL.md"), - ]; - for (const candidate of candidates) { - if (existsSync(candidate)) { - return candidate; - } + const skillPath = join(workspaceDir, "skills", skillName, "SKILL.md"); + if (existsSync(skillPath)) { + return skillPath; } - // Default to workspace skills dir for new files - return candidates[0]; + // Default to workspace skills dir for new files. + return skillPath; } if (virtualPath.startsWith("~memories/")) { @@ -83,11 +79,12 @@ function resolveVirtualPath(virtualPath: string): string | null { * Double-check that the resolved path stays within expected directories. */ function isSafePath(absPath: string): boolean { - const stateDir = resolveOpenClawStateDir(); - const workspaceDir = resolveWorkspaceRoot() ?? join(stateDir, "workspace"); + const workspaceDir = resolveWorkspaceRoot(); + if (!workspaceDir) { + return false; + } const normalized = normalize(resolve(absPath)); const allowed = [ - normalize(join(stateDir, "skills")), normalize(join(workspaceDir, "skills")), normalize(workspaceDir), ];