2026-02-21 13:10:32 -08:00
|
|
|
import { readdirSync, readFileSync, existsSync, statSync, type Dirent } from "node:fs";
|
2026-02-11 16:45:07 -08:00
|
|
|
import { join } from "node:path";
|
2026-02-19 14:59:34 -08:00
|
|
|
import { resolveWorkspaceRoot, resolveOpenClawStateDir, getEffectiveProfile, parseSimpleYaml, duckdbQueryAll, isDatabaseFile } from "@/lib/workspace";
|
2026-02-11 16:45:07 -08:00
|
|
|
|
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
|
|
|
|
|
|
export type TreeNode = {
|
|
|
|
|
name: string;
|
2026-02-15 18:18:15 -08:00
|
|
|
path: string; // relative to workspace root (or ~skills/ for virtual nodes)
|
2026-02-11 18:35:35 -08:00
|
|
|
type: "object" | "document" | "folder" | "file" | "database" | "report";
|
2026-02-11 16:45:07 -08:00
|
|
|
icon?: string;
|
|
|
|
|
defaultView?: "table" | "kanban";
|
|
|
|
|
children?: TreeNode[];
|
2026-02-15 18:18:15 -08:00
|
|
|
/** Virtual nodes live outside the main workspace (e.g. Skills, Memories). */
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
virtual?: boolean;
|
2026-02-21 13:10:32 -08:00
|
|
|
/** True when the entry is a symbolic link. */
|
|
|
|
|
symlink?: boolean;
|
2026-02-11 16:45:07 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type DbObject = {
|
|
|
|
|
name: string;
|
|
|
|
|
icon?: string;
|
|
|
|
|
default_view?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Read .object.yaml metadata from a directory if it exists. */
|
|
|
|
|
function readObjectMeta(
|
|
|
|
|
dirPath: string,
|
|
|
|
|
): { icon?: string; defaultView?: string } | null {
|
|
|
|
|
const yamlPath = join(dirPath, ".object.yaml");
|
|
|
|
|
if (!existsSync(yamlPath)) {return null;}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const content = readFileSync(yamlPath, "utf-8");
|
|
|
|
|
const parsed = parseSimpleYaml(content);
|
|
|
|
|
return {
|
|
|
|
|
icon: parsed.icon as string | undefined,
|
|
|
|
|
defaultView: parsed.default_view as string | undefined,
|
|
|
|
|
};
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-15 23:00:25 -08:00
|
|
|
* Query ALL discovered DuckDB files for objects so we can identify object
|
|
|
|
|
* directories even when .object.yaml files are missing.
|
|
|
|
|
* Shallower databases win on name conflicts (parent priority).
|
2026-02-11 16:45:07 -08:00
|
|
|
*/
|
|
|
|
|
function loadDbObjects(): Map<string, DbObject> {
|
|
|
|
|
const map = new Map<string, DbObject>();
|
2026-02-15 23:00:25 -08:00
|
|
|
const rows = duckdbQueryAll<DbObject & { name: string }>(
|
2026-02-11 16:45:07 -08:00
|
|
|
"SELECT name, icon, default_view FROM objects",
|
2026-02-15 23:00:25 -08:00
|
|
|
"name",
|
2026-02-11 16:45:07 -08:00
|
|
|
);
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
map.set(row.name, row);
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 13:10:32 -08:00
|
|
|
/** Resolve a dirent's effective type, following symlinks to their target. */
|
|
|
|
|
function resolveEntryType(entry: Dirent, absPath: string): "directory" | "file" | null {
|
|
|
|
|
if (entry.isDirectory()) {return "directory";}
|
|
|
|
|
if (entry.isFile()) {return "file";}
|
|
|
|
|
if (entry.isSymbolicLink()) {
|
|
|
|
|
try {
|
|
|
|
|
const st = statSync(absPath);
|
|
|
|
|
if (st.isDirectory()) {return "directory";}
|
|
|
|
|
if (st.isFile()) {return "file";}
|
|
|
|
|
} catch {
|
|
|
|
|
// Broken symlink -- skip
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
feat(web): full UI redesign with light/dark theme, TanStack data tables, media rendering, and gateway-routed agent execution
Overhaul the Dench web app with a comprehensive visual redesign and several
major feature additions across the chat interface, workspace, and agent
runtime layer.
Theme & Design System
- Replace the dark-only palette with a full light/dark theme system that
respects system preference via localStorage + inline script (no FOUC).
- Introduce new design tokens: glassmorphism surfaces, semantic colors
(success/warning/error/info), object-type chip palettes, and a tiered
shadow scale (sm/md/lg/xl).
- Add Instrument Serif + Inter via Google Fonts for a refined typographic
hierarchy; headings use the serif face, body uses Inter.
- Rebrand UI from "Ironclaw" to "Dench" across the landing page and
metadata.
Chat & Chain-of-Thought
- Rewrite the chain-of-thought component with inline media detection and
rendering — images, video, audio, and PDFs referenced in agent output
are now displayed directly in the conversation thread.
- Add status indicator parts (e.g. "Preparing response...",
"Optimizing session context...") that render as subtle activity badges
instead of verbose reasoning blocks.
- Integrate react-markdown with remark-gfm for proper markdown rendering
in assistant messages (tables, strikethrough, autolinks, etc.).
- Improve report-block splitting and lazy-loaded ReportCard rendering.
Workspace
- Introduce @tanstack/react-table for the object table, replacing the
hand-rolled table with full column sorting, fuzzy filtering via
match-sorter-utils, row selection, and bulk actions.
- Add a new media viewer component for in-workspace image/video/PDF
preview.
- New API routes: bulk-delete entries, field management (CRUD + reorder),
raw-file serving endpoint for media assets.
- Redesign workspace sidebar, empty state, and entry detail modal with
the new theme tokens and improved layout.
Agent Runtime
- Switch web agent execution from --local to gateway-routed mode so
concurrent chat threads share the gateway's lane-based concurrency
system, eliminating cross-process file-lock contention.
- Advertise "tool-events" capability during WebSocket handshake so the
gateway streams tool start/update/result events to the UI.
- Add new agent callback hooks: onLifecycleStart, onCompactionStart/End,
and onToolUpdate for richer real-time feedback.
- Forward media URLs emitted by agent events into the chat stream.
Dependencies
- Add @tanstack/match-sorter-utils and @tanstack/react-table to the web
app.
Published as ironclaw@2026.2.10-1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 11:17:23 -08:00
|
|
|
/** Recursively build a tree from a workspace directory. */
|
2026-02-11 16:45:07 -08:00
|
|
|
function buildTree(
|
|
|
|
|
absDir: string,
|
|
|
|
|
relativeBase: string,
|
|
|
|
|
dbObjects: Map<string, DbObject>,
|
2026-02-21 13:10:32 -08:00
|
|
|
showHidden = false,
|
2026-02-11 16:45:07 -08:00
|
|
|
): TreeNode[] {
|
|
|
|
|
const nodes: TreeNode[] = [];
|
|
|
|
|
|
|
|
|
|
let entries: Dirent[];
|
|
|
|
|
try {
|
|
|
|
|
entries = readdirSync(absDir, { withFileTypes: true });
|
|
|
|
|
} catch {
|
|
|
|
|
return nodes;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 13:10:32 -08:00
|
|
|
const filtered = entries.filter((e) => {
|
|
|
|
|
// .object.yaml is always needed for metadata; also shown as a node when showHidden is on
|
|
|
|
|
if (e.name === ".object.yaml") {return true;}
|
|
|
|
|
if (e.name.startsWith(".")) {return showHidden;}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-11 16:45:07 -08:00
|
|
|
// Sort: directories first, then files, alphabetical within each group
|
2026-02-21 13:10:32 -08:00
|
|
|
const sorted = filtered.toSorted((a, b) => {
|
|
|
|
|
const absA = join(absDir, a.name);
|
|
|
|
|
const absB = join(absDir, b.name);
|
|
|
|
|
const typeA = resolveEntryType(a, absA);
|
|
|
|
|
const typeB = resolveEntryType(b, absB);
|
|
|
|
|
const dirA = typeA === "directory";
|
|
|
|
|
const dirB = typeB === "directory";
|
|
|
|
|
if (dirA && !dirB) {return -1;}
|
|
|
|
|
if (!dirA && dirB) {return 1;}
|
|
|
|
|
return a.name.localeCompare(b.name);
|
|
|
|
|
});
|
2026-02-11 16:45:07 -08:00
|
|
|
|
|
|
|
|
for (const entry of sorted) {
|
2026-02-21 13:10:32 -08:00
|
|
|
// .object.yaml is consumed for metadata; only show it as a visible node when revealing hidden files
|
|
|
|
|
if (entry.name === ".object.yaml" && !showHidden) {continue;}
|
2026-02-11 16:45:07 -08:00
|
|
|
|
|
|
|
|
const absPath = join(absDir, entry.name);
|
|
|
|
|
const relPath = relativeBase
|
|
|
|
|
? `${relativeBase}/${entry.name}`
|
|
|
|
|
: entry.name;
|
|
|
|
|
|
2026-02-21 13:10:32 -08:00
|
|
|
const isSymlink = entry.isSymbolicLink();
|
|
|
|
|
const effectiveType = resolveEntryType(entry, absPath);
|
|
|
|
|
|
|
|
|
|
if (effectiveType === "directory") {
|
2026-02-11 16:45:07 -08:00
|
|
|
const objectMeta = readObjectMeta(absPath);
|
|
|
|
|
const dbObject = dbObjects.get(entry.name);
|
2026-02-21 13:10:32 -08:00
|
|
|
const children = buildTree(absPath, relPath, dbObjects, showHidden);
|
2026-02-11 16:45:07 -08:00
|
|
|
|
|
|
|
|
if (objectMeta || dbObject) {
|
|
|
|
|
nodes.push({
|
|
|
|
|
name: entry.name,
|
|
|
|
|
path: relPath,
|
|
|
|
|
type: "object",
|
|
|
|
|
icon: objectMeta?.icon ?? dbObject?.icon,
|
|
|
|
|
defaultView:
|
|
|
|
|
((objectMeta?.defaultView ?? dbObject?.default_view) as
|
|
|
|
|
| "table"
|
|
|
|
|
| "kanban") ?? "table",
|
|
|
|
|
children: children.length > 0 ? children : undefined,
|
2026-02-21 13:10:32 -08:00
|
|
|
...(isSymlink && { symlink: true }),
|
2026-02-11 16:45:07 -08:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
nodes.push({
|
|
|
|
|
name: entry.name,
|
|
|
|
|
path: relPath,
|
|
|
|
|
type: "folder",
|
|
|
|
|
children: children.length > 0 ? children : undefined,
|
2026-02-21 13:10:32 -08:00
|
|
|
...(isSymlink && { symlink: true }),
|
2026-02-11 16:45:07 -08:00
|
|
|
});
|
|
|
|
|
}
|
2026-02-21 13:10:32 -08:00
|
|
|
} else if (effectiveType === "file") {
|
2026-02-11 16:45:07 -08:00
|
|
|
const ext = entry.name.split(".").pop()?.toLowerCase();
|
2026-02-11 18:35:35 -08:00
|
|
|
const isReport = entry.name.endsWith(".report.json");
|
2026-02-11 16:45:07 -08:00
|
|
|
const isDocument = ext === "md" || ext === "mdx";
|
2026-02-11 17:01:28 -08:00
|
|
|
const isDatabase = isDatabaseFile(entry.name);
|
2026-02-11 16:45:07 -08:00
|
|
|
|
|
|
|
|
nodes.push({
|
|
|
|
|
name: entry.name,
|
|
|
|
|
path: relPath,
|
2026-02-11 18:35:35 -08:00
|
|
|
type: isReport ? "report" : isDatabase ? "database" : isDocument ? "document" : "file",
|
2026-02-21 13:10:32 -08:00
|
|
|
...(isSymlink && { symlink: true }),
|
2026-02-11 16:45:07 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nodes;
|
|
|
|
|
}
|
|
|
|
|
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
// --- Virtual folder builders ---
|
|
|
|
|
|
|
|
|
|
/** Parse YAML frontmatter from a SKILL.md file (lightweight). */
|
|
|
|
|
function parseSkillFrontmatter(content: string): { name?: string; emoji?: string } {
|
|
|
|
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
|
|
|
if (!match) {return {};}
|
|
|
|
|
const yaml = match[1];
|
|
|
|
|
const result: Record<string, string> = {};
|
|
|
|
|
for (const line of yaml.split("\n")) {
|
|
|
|
|
const kv = line.match(/^(\w+)\s*:\s*(.+)/);
|
|
|
|
|
if (kv) {result[kv[1]] = kv[2].replace(/^["']|["']$/g, "").trim();}
|
|
|
|
|
}
|
|
|
|
|
return { name: result.name, emoji: result.emoji };
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:59:34 -08:00
|
|
|
/** Build a virtual "Skills" folder from <stateDir>/skills/. */
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
function buildSkillsVirtualFolder(): TreeNode | null {
|
2026-02-19 14:59:34 -08:00
|
|
|
const stateDir = resolveOpenClawStateDir();
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
const dirs = [
|
2026-02-19 14:59:34 -08:00
|
|
|
join(stateDir, "skills"),
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const children: TreeNode[] = [];
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
|
|
|
|
for (const dir of dirs) {
|
|
|
|
|
if (!existsSync(dir)) {continue;}
|
|
|
|
|
try {
|
|
|
|
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (!entry.isDirectory() || seen.has(entry.name)) {continue;}
|
|
|
|
|
const skillMdPath = join(dir, entry.name, "SKILL.md");
|
|
|
|
|
if (!existsSync(skillMdPath)) {continue;}
|
|
|
|
|
|
|
|
|
|
seen.add(entry.name);
|
|
|
|
|
let displayName = entry.name;
|
|
|
|
|
try {
|
|
|
|
|
const content = readFileSync(skillMdPath, "utf-8");
|
|
|
|
|
const meta = parseSkillFrontmatter(content);
|
|
|
|
|
if (meta.name) {displayName = meta.name;}
|
|
|
|
|
if (meta.emoji) {displayName = `${meta.emoji} ${displayName}`;}
|
|
|
|
|
} catch {
|
|
|
|
|
// skip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
children.push({
|
|
|
|
|
name: displayName,
|
|
|
|
|
path: `~skills/${entry.name}/SKILL.md`,
|
|
|
|
|
type: "document",
|
|
|
|
|
virtual: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// dir unreadable
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (children.length === 0) {return null;}
|
|
|
|
|
children.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name: "Skills",
|
|
|
|
|
path: "~skills",
|
|
|
|
|
type: "folder",
|
|
|
|
|
virtual: true,
|
|
|
|
|
children,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-02-21 13:10:32 -08:00
|
|
|
export async function GET(req: Request) {
|
|
|
|
|
const url = new URL(req.url);
|
|
|
|
|
const showHidden = url.searchParams.get("showHidden") === "1";
|
|
|
|
|
|
2026-02-19 14:59:34 -08:00
|
|
|
const openclawDir = resolveOpenClawStateDir();
|
|
|
|
|
const profile = getEffectiveProfile();
|
2026-02-15 18:18:15 -08:00
|
|
|
const root = resolveWorkspaceRoot();
|
2026-02-11 16:45:07 -08:00
|
|
|
if (!root) {
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
const tree: TreeNode[] = [];
|
|
|
|
|
const skillsFolder = buildSkillsVirtualFolder();
|
|
|
|
|
if (skillsFolder) {tree.push(skillsFolder);}
|
2026-02-19 14:59:34 -08:00
|
|
|
return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir, profile });
|
2026-02-11 16:45:07 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dbObjects = loadDbObjects();
|
|
|
|
|
|
2026-02-21 13:10:32 -08:00
|
|
|
const tree = buildTree(root, "", dbObjects, showHidden);
|
2026-02-11 16:45:07 -08:00
|
|
|
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
const skillsFolder = buildSkillsVirtualFolder();
|
|
|
|
|
if (skillsFolder) {tree.push(skillsFolder);}
|
|
|
|
|
|
2026-02-19 14:59:34 -08:00
|
|
|
return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir, profile });
|
2026-02-11 16:45:07 -08:00
|
|
|
}
|