diff --git a/apps/web/app/api/workspace/browse/route.ts b/apps/web/app/api/workspace/browse/route.ts index 3ca85dc019c..3304bfbb29e 100644 --- a/apps/web/app/api/workspace/browse/route.ts +++ b/apps/web/app/api/workspace/browse/route.ts @@ -1,6 +1,6 @@ import { readdirSync, type Dirent } from "node:fs"; import { join, dirname, resolve } from "node:path"; -import { resolveDenchRoot } from "@/lib/workspace"; +import { resolveWorkspaceRoot } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -72,9 +72,9 @@ export async function GET(req: Request) { const url = new URL(req.url); let dir = url.searchParams.get("dir"); - // Default to the dench workspace root + // Default to the workspace root if (!dir) { - dir = resolveDenchRoot(); + dir = resolveWorkspaceRoot(); } if (!dir) { diff --git a/apps/web/app/api/workspace/context/route.ts b/apps/web/app/api/workspace/context/route.ts index 37e0e5f31ae..c9af98b8f19 100644 --- a/apps/web/app/api/workspace/context/route.ts +++ b/apps/web/app/api/workspace/context/route.ts @@ -1,6 +1,6 @@ import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { resolveDenchRoot } from "@/lib/workspace"; +import { resolveWorkspaceRoot } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -27,7 +27,7 @@ export type WorkspaceContext = { /** * Parse workspace_context.yaml with basic YAML extraction. - * Handles the specific structure defined by the Dench skill. + * Handles the specific structure defined by the workspace skill. */ function parseWorkspaceContext(content: string): WorkspaceContext { const ctx: WorkspaceContext = { exists: true }; @@ -96,7 +96,7 @@ function parseWorkspaceContext(content: string): WorkspaceContext { } export async function GET() { - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) { return Response.json({ exists: false } satisfies WorkspaceContext); } diff --git a/apps/web/app/api/workspace/db/introspect/route.ts b/apps/web/app/api/workspace/db/introspect/route.ts index 3c78a50cea0..409c3fb3e96 100644 --- a/apps/web/app/api/workspace/db/introspect/route.ts +++ b/apps/web/app/api/workspace/db/introspect/route.ts @@ -1,4 +1,4 @@ -import { safeResolvePath, duckdbQueryOnFile } from "@/lib/workspace"; +import { safeResolvePath, duckdbQueryOnFile, resolveDuckdbBin } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -39,6 +39,11 @@ export async function GET(request: Request) { ); } + // Check if DuckDB CLI binary is available + if (!resolveDuckdbBin()) { + return Response.json({ tables: [], path: relPath, duckdb_available: false }); + } + // Get all user tables (skip internal DuckDB catalogs) const rawTables = duckdbQueryOnFile<{ table_name: string; diff --git a/apps/web/app/api/workspace/file/route.ts b/apps/web/app/api/workspace/file/route.ts index 6f51a6be274..a096c66f9b7 100644 --- a/apps/web/app/api/workspace/file/route.ts +++ b/apps/web/app/api/workspace/file/route.ts @@ -31,7 +31,7 @@ export async function GET(req: Request) { * POST /api/workspace/file * Body: { path: string, content: string } * - * Writes a file to the dench workspace. Creates parent directories as needed. + * Writes a file to the workspace. Creates parent directories as needed. */ export async function POST(req: Request) { let body: { path?: string; content?: string }; @@ -74,7 +74,7 @@ export async function POST(req: Request) { * DELETE /api/workspace/file * Body: { path: string } * - * Deletes a file or folder from the dench workspace. + * Deletes a file or folder from the workspace. * System files (.object.yaml, workspace.duckdb, etc.) are protected. */ export async function DELETE(req: Request) { diff --git a/apps/web/app/api/workspace/mkdir/route.ts b/apps/web/app/api/workspace/mkdir/route.ts index 2414acdf594..14b513956e2 100644 --- a/apps/web/app/api/workspace/mkdir/route.ts +++ b/apps/web/app/api/workspace/mkdir/route.ts @@ -8,7 +8,7 @@ export const runtime = "nodejs"; * POST /api/workspace/mkdir * Body: { path: string } * - * Creates a new directory in the dench workspace. + * Creates a new directory in the workspace. */ export async function POST(req: Request) { let body: { path?: string }; diff --git a/apps/web/app/api/workspace/objects/[name]/route.ts b/apps/web/app/api/workspace/objects/[name]/route.ts index 086abf6f4c6..ddb493b3395 100644 --- a/apps/web/app/api/workspace/objects/[name]/route.ts +++ b/apps/web/app/api/workspace/objects/[name]/route.ts @@ -1,4 +1,4 @@ -import { duckdbQuery, duckdbPath, duckdbExec, parseRelationValue } from "@/lib/workspace"; +import { duckdbQuery, duckdbPath, duckdbExec, parseRelationValue, resolveDuckdbBin } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -315,6 +315,13 @@ export async function GET( ) { const { name } = await params; + if (!resolveDuckdbBin()) { + return Response.json( + { error: "DuckDB CLI is not installed", code: "DUCKDB_NOT_INSTALLED" }, + { status: 503 }, + ); + } + if (!duckdbPath()) { return Response.json( { error: "DuckDB database not found" }, diff --git a/apps/web/app/api/workspace/raw-file/route.ts b/apps/web/app/api/workspace/raw-file/route.ts index 69b859e5c3a..82861ef8073 100644 --- a/apps/web/app/api/workspace/raw-file/route.ts +++ b/apps/web/app/api/workspace/raw-file/route.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; -import { safeResolvePath, resolveDenchRoot } from "@/lib/workspace"; +import { safeResolvePath, resolveWorkspaceRoot } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -57,7 +57,7 @@ function resolveFile(path: string): string | null { if (resolved) {return resolved;} // 3. Try common subdirectories in case the path is a bare filename - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) {return null;} const rootAbs = resolve(root); const basename = path.split("/").pop() ?? path; diff --git a/apps/web/app/api/workspace/search-index/route.ts b/apps/web/app/api/workspace/search-index/route.ts index 342cd44108e..e0425354360 100644 --- a/apps/web/app/api/workspace/search-index/route.ts +++ b/apps/web/app/api/workspace/search-index/route.ts @@ -1,7 +1,7 @@ import { readdirSync, readFileSync, existsSync, type Dirent } from "node:fs"; import { join } from "node:path"; import { - resolveDenchRoot, + resolveWorkspaceRoot, parseSimpleYaml, duckdbQuery, duckdbPath, @@ -243,7 +243,7 @@ export async function GET() { const items: SearchIndexItem[] = []; // 1. Files + objects from tree - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (root) { const dbObjects = new Map(); if (duckdbPath()) { @@ -253,7 +253,7 @@ export async function GET() { for (const o of objs) {dbObjects.set(o.name, o);} } - // Scan entire dench root (the dench folder IS the knowledge base) + // Scan workspace root (the workspace folder IS the knowledge base) flattenTree(root, "", dbObjects, items); } diff --git a/apps/web/app/api/workspace/suggest-files/route.ts b/apps/web/app/api/workspace/suggest-files/route.ts index 110741fa178..e916e4ee21d 100644 --- a/apps/web/app/api/workspace/suggest-files/route.ts +++ b/apps/web/app/api/workspace/suggest-files/route.ts @@ -1,7 +1,7 @@ import { readdirSync, type Dirent } from "node:fs"; import { join, dirname, resolve, basename } from "node:path"; import { homedir } from "node:os"; -import { resolveDenchRoot } from "@/lib/workspace"; +import { resolveWorkspaceRoot } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -186,7 +186,7 @@ export async function GET(req: Request) { const url = new URL(req.url); const pathQuery = url.searchParams.get("path"); const searchQuery = url.searchParams.get("q"); - const workspaceRoot = resolveDenchRoot() ?? homedir(); + const workspaceRoot = resolveWorkspaceRoot() ?? homedir(); // Search mode: find files by name if (searchQuery) { diff --git a/apps/web/app/api/workspace/tree/route.ts b/apps/web/app/api/workspace/tree/route.ts index cfc9962e4a5..5e98b36c0b7 100644 --- a/apps/web/app/api/workspace/tree/route.ts +++ b/apps/web/app/api/workspace/tree/route.ts @@ -1,19 +1,19 @@ import { readdirSync, readFileSync, existsSync, type Dirent } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { resolveDenchRoot, parseSimpleYaml, duckdbQuery, isDatabaseFile } from "@/lib/workspace"; +import { resolveWorkspaceRoot, parseSimpleYaml, duckdbQuery, isDatabaseFile } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; export type TreeNode = { name: string; - path: string; // relative to dench/ (or ~skills/, ~memories/, ~workspace/ for virtual nodes) + path: string; // relative to workspace root (or ~skills/ for virtual nodes) type: "object" | "document" | "folder" | "file" | "database" | "report"; icon?: string; defaultView?: "table" | "kanban"; children?: TreeNode[]; - /** Virtual nodes live outside the dench workspace (e.g. Skills, Memories). */ + /** Virtual nodes live outside the main workspace (e.g. Skills, Memories). */ virtual?: boolean; }; @@ -150,12 +150,11 @@ function parseSkillFrontmatter(content: string): { name?: string; emoji?: string return { name: result.name, emoji: result.emoji }; } -/** Build a virtual "Skills" folder from ~/.openclaw/skills/ and ~/.openclaw/workspace/skills/. */ +/** Build a virtual "Skills" folder from ~/.openclaw/skills/. */ function buildSkillsVirtualFolder(): TreeNode | null { const home = homedir(); const dirs = [ join(home, ".openclaw", "skills"), - join(home, ".openclaw", "workspace", "skills"), ]; const children: TreeNode[] = []; @@ -205,134 +204,30 @@ function buildSkillsVirtualFolder(): TreeNode | null { }; } -/** - * Build top-level workspace root file nodes (USER.md, SOUL.md, TOOLS.md, etc.). - * These live directly in ~/.openclaw/workspace/ but outside the dench/ subdirectory. - * They are virtual (not movable/renamable/deletable) but editable. - */ -function buildWorkspaceRootFiles(): TreeNode[] { - const workspaceDir = join(homedir(), ".openclaw", "workspace"); - if (!existsSync(workspaceDir)) {return [];} - - // Files already handled by the Memories virtual folder - const SKIP_FILES = new Set(["MEMORY.md", "memory.md"]); - - const nodes: TreeNode[] = []; - - try { - const entries = readdirSync(workspaceDir, { withFileTypes: true }); - for (const entry of entries) { - // Skip subdirectories (handled elsewhere) and hidden files - if (entry.isDirectory()) {continue;} - if (entry.name.startsWith(".")) {continue;} - if (SKIP_FILES.has(entry.name)) {continue;} - - const ext = entry.name.split(".").pop()?.toLowerCase(); - const isDocument = ext === "md" || ext === "mdx"; - - nodes.push({ - name: entry.name, - path: `~workspace/${entry.name}`, - type: isDocument ? "document" : "file", - virtual: true, - }); - } - } catch { - // dir unreadable - } - - // Sort alphabetically - nodes.sort((a, b) => a.name.localeCompare(b.name)); - return nodes; -} - -/** Build a virtual "Memories" folder from ~/.openclaw/workspace/. */ -function buildMemoriesVirtualFolder(): TreeNode | null { - const workspaceDir = join(homedir(), ".openclaw", "workspace"); - const children: TreeNode[] = []; - - // MEMORY.md - for (const filename of ["MEMORY.md", "memory.md"]) { - const memPath = join(workspaceDir, filename); - if (existsSync(memPath)) { - children.push({ - name: "MEMORY.md", - path: `~memories/MEMORY.md`, - type: "document", - virtual: true, - }); - break; - } - } - - // Daily logs from memory/ - const memoryDir = join(workspaceDir, "memory"); - if (existsSync(memoryDir)) { - try { - const entries = readdirSync(memoryDir, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isFile() || !entry.name.endsWith(".md")) {continue;} - children.push({ - name: entry.name, - path: `~memories/${entry.name}`, - type: "document", - virtual: true, - }); - } - } catch { - // dir unreadable - } - } - - if (children.length === 0) {return null;} - // Sort: MEMORY.md first, then reverse chronological for daily logs - children.sort((a, b) => { - if (a.name === "MEMORY.md") {return -1;} - if (b.name === "MEMORY.md") {return 1;} - return b.name.localeCompare(a.name); - }); - - return { - name: "Memories", - path: "~memories", - type: "folder", - virtual: true, - children, - }; -} export async function GET() { const home = homedir(); const openclawDir = join(home, ".openclaw"); - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) { - // Even without a dench workspace, return virtual folders if they exist + // Even without a workspace, return virtual folders if they exist const tree: TreeNode[] = []; - tree.push(...buildWorkspaceRootFiles()); const skillsFolder = buildSkillsVirtualFolder(); if (skillsFolder) {tree.push(skillsFolder);} - const memoriesFolder = buildMemoriesVirtualFolder(); - if (memoriesFolder) {tree.push(memoriesFolder);} return Response.json({ tree, exists: false, workspaceRoot: null, openclawDir }); } // Load objects from DuckDB for smart directory detection const dbObjects = loadDbObjects(); - // Scan the entire dench root -- the dench folder IS the knowledge base. - // All top-level directories (manufacturing, knowledge, reports, etc.) - // and files are visible in the sidebar. + // Scan the workspace root — it IS the knowledge base. + // All top-level directories, files, objects, and documents are visible + // in the sidebar (USER.md, SOUL.md, memory/, etc. are all part of the tree). const tree = buildTree(root, "", dbObjects); - // Workspace root files (USER.md, SOUL.md, etc.) -- editable but reserved - const workspaceRootFiles = buildWorkspaceRootFiles(); - if (workspaceRootFiles.length > 0) {tree.push(...workspaceRootFiles);} - // Virtual folders go after all real files/folders const skillsFolder = buildSkillsVirtualFolder(); if (skillsFolder) {tree.push(skillsFolder);} - const memoriesFolder = buildMemoriesVirtualFolder(); - if (memoriesFolder) {tree.push(memoriesFolder);} return Response.json({ tree, exists: true, workspaceRoot: root, openclawDir }); } diff --git a/apps/web/app/api/workspace/upload/route.ts b/apps/web/app/api/workspace/upload/route.ts index 84398db08c1..aeabef0585b 100644 --- a/apps/web/app/api/workspace/upload/route.ts +++ b/apps/web/app/api/workspace/upload/route.ts @@ -1,6 +1,6 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { join, dirname, extname } from "node:path"; -import { resolveDenchRoot, safeResolveNewPath } from "@/lib/workspace"; +import { resolveWorkspaceRoot, safeResolveNewPath } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -18,7 +18,7 @@ const MAX_SIZE = 10 * 1024 * 1024; // 10 MB * Returns { ok, path } where path is workspace-relative. */ export async function POST(req: Request) { - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) { return Response.json( { error: "Workspace not found" }, diff --git a/apps/web/app/api/workspace/watch/route.ts b/apps/web/app/api/workspace/watch/route.ts index 72e0e2e6710..e9062fcc119 100644 --- a/apps/web/app/api/workspace/watch/route.ts +++ b/apps/web/app/api/workspace/watch/route.ts @@ -1,4 +1,4 @@ -import { resolveDenchRoot } from "@/lib/workspace"; +import { resolveWorkspaceRoot } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -6,12 +6,12 @@ export const runtime = "nodejs"; /** * GET /api/workspace/watch * - * Server-Sent Events endpoint that watches the dench workspace for file changes. + * Server-Sent Events endpoint that watches the workspace for file changes. * Sends events: { type: "add"|"change"|"unlink"|"addDir"|"unlinkDir", path: string } * Falls back gracefully if chokidar is unavailable. */ export async function GET() { - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) { return new Response("Workspace not found", { status: 404 }); } diff --git a/apps/web/app/components/chat-panel.tsx b/apps/web/app/components/chat-panel.tsx index 509e1eb454f..425cd5eaf22 100644 --- a/apps/web/app/components/chat-panel.tsx +++ b/apps/web/app/components/chat-panel.tsx @@ -419,6 +419,8 @@ function createStreamParser() { export type ChatPanelHandle = { loadSession: (sessionId: string) => Promise; newSession: () => Promise; + /** Create a new session and immediately send a message. */ + sendNewMessage: (text: string) => Promise; /** Insert a file mention into the chat editor (e.g. from sidebar drag). */ insertFileMention?: (name: string, path: string) => void; }; @@ -1071,11 +1073,23 @@ export const ChatPanel = forwardRef( () => ({ loadSession: handleSessionSelect, newSession: handleNewSession, + sendNewMessage: async (text: string) => { + await handleNewSession(); + const title = + text.length > 60 ? text.slice(0, 60) + "..." : text; + const sessionId = await createSession(title); + setCurrentSessionId(sessionId); + sessionIdRef.current = sessionId; + onActiveSessionChange?.(sessionId); + onSessionsChange?.(); + userScrolledAwayRef.current = false; + void sendMessage({ text }); + }, insertFileMention: (name: string, path: string) => { editorRef.current?.insertFileMention(name, path); }, }), - [handleSessionSelect, handleNewSession], + [handleSessionSelect, handleNewSession, createSession, onActiveSessionChange, onSessionsChange, sendMessage], ); // ── Stop handler (aborts server-side run + client-side stream) ── diff --git a/apps/web/app/components/workspace/database-viewer.tsx b/apps/web/app/components/workspace/database-viewer.tsx index a4f3a092422..b0813f47d19 100644 --- a/apps/web/app/components/workspace/database-viewer.tsx +++ b/apps/web/app/components/workspace/database-viewer.tsx @@ -23,7 +23,7 @@ type SortState = { } | null; type DatabaseViewerProps = { - /** Relative path to the database file within the dench workspace */ + /** Relative path to the database file within the workspace */ dbPath: string; filename: string; }; @@ -127,12 +127,66 @@ function typeDisplay(dtype: string): { label: string; color: string } { return { label: dtype.toLowerCase(), color: "var(--color-text-muted)" }; } +// --- DuckDB Not Installed Panel --- + +/** Shown when the DuckDB CLI binary cannot be found on the system. */ +export function DuckDBMissing() { + return ( +
+
+ +
+
+

+ DuckDB is not installed +

+

+ The DuckDB CLI is required to view database files and workspace data. + Click below to install it automatically. +

+
+ +
+ ); +} + // --- Main Component --- export function DatabaseViewer({ dbPath, filename }: DatabaseViewerProps) { const [tables, setTables] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [duckdbAvailable, setDuckdbAvailable] = useState(true); // Selected table const [selectedTable, setSelectedTable] = useState(null); @@ -173,10 +227,14 @@ export function DatabaseViewer({ dbPath, filename }: DatabaseViewerProps) { } const data = await res.json(); if (!cancelled) { - setTables(data.tables ?? []); - // Auto-select first table - if (data.tables?.length > 0) { - setSelectedTable(data.tables[0].table_name); + if (data.duckdb_available === false) { + setDuckdbAvailable(false); + } else { + setTables(data.tables ?? []); + // Auto-select first table + if (data.tables?.length > 0) { + setSelectedTable(data.tables[0].table_name); + } } } } catch (err) { @@ -330,6 +388,11 @@ export function DatabaseViewer({ dbPath, filename }: DatabaseViewerProps) { ); } + // --- DuckDB not installed --- + if (!duckdbAvailable) { + return ; + } + return (
{/* Left panel: Table list */} diff --git a/apps/web/app/components/workspace/empty-state.tsx b/apps/web/app/components/workspace/empty-state.tsx index db7a0203340..2d3cb185e86 100644 --- a/apps/web/app/components/workspace/empty-state.tsx +++ b/apps/web/app/components/workspace/empty-state.tsx @@ -69,22 +69,22 @@ export function EmptyState({ className="text-sm leading-relaxed" style={{ color: "var(--color-text-muted)" }} > - {workspaceExists ? ( - <> - The Dench workspace exists but has no - knowledge tree yet. Ask the CRM agent to - create objects and documents to populate - it. - - ) : ( - <> - The Dench workspace directory was not - found. To initialize it, start a - conversation with the CRM agent and it - will create the workspace structure - automatically. - - )} + {workspaceExists ? ( + <> + The workspace exists but has no + knowledge tree yet. Ask the CRM agent to + create objects and documents to populate + it. + + ) : ( + <> + The workspace directory was not + found. To initialize it, start a + conversation with the CRM agent and it + will create the workspace structure + automatically. + + )}

@@ -125,7 +125,7 @@ export function EmptyState({ border: "1px solid var(--color-border)", }} > - ~/.openclaw/workspace/dench/ + ~/.openclaw/workspace diff --git a/apps/web/app/components/workspace/markdown-editor.tsx b/apps/web/app/components/workspace/markdown-editor.tsx index a2802abcf2f..050de331fea 100644 --- a/apps/web/app/components/workspace/markdown-editor.tsx +++ b/apps/web/app/components/workspace/markdown-editor.tsx @@ -276,7 +276,7 @@ export function MarkdownEditor({ // Prepend preserved frontmatter so it isn't lost on save const finalContent = frontmatterRef.current + bodyContent; - // Virtual paths (~skills/*, ~memories/*) use the virtual-file API + // Virtual paths (~skills/*) use the virtual-file API const saveEndpoint = filePath.startsWith("~") ? "/api/workspace/virtual-file" : "/api/workspace/file"; diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index 15ea44ecc8f..2e32b93498d 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -11,7 +11,7 @@ import { DocumentView } from "../components/workspace/document-view"; import { FileViewer } from "../components/workspace/file-viewer"; import { CodeViewer } from "../components/workspace/code-viewer"; import { MediaViewer, detectMediaType, type MediaType } from "../components/workspace/media-viewer"; -import { DatabaseViewer } from "../components/workspace/database-viewer"; +import { DatabaseViewer, DuckDBMissing } from "../components/workspace/database-viewer"; import { Breadcrumbs } from "../components/workspace/breadcrumbs"; import { ChatSessionsSidebar } from "../components/workspace/chat-sessions-sidebar"; import { EmptyState } from "../components/workspace/empty-state"; @@ -103,7 +103,7 @@ type WebSession = { // --- Helpers --- -/** Detect virtual paths (skills, memories) that live outside the dench workspace. */ +/** Detect virtual paths (skills, memories) that live outside the main workspace. */ function isVirtualPath(path: string): boolean { return path.startsWith("~"); } @@ -337,7 +337,7 @@ function WorkspacePageInner() { const data: ObjectData = await res.json(); setContent({ kind: "object", data }); } else if (node.type === "document") { - // Use virtual-file API for ~skills/ and ~memories/ paths + // Use virtual-file API for ~skills/ paths const res = await fetch(fileApiUrl(node.path)); if (!res.ok) { setContent({ kind: "none" }); @@ -391,7 +391,7 @@ function WorkspacePageInner() { // filesystem, switch back to workspace mode or show the appropriate // dashboard instead of showing raw files. if (browseDir && isAbsolutePath(node.path)) { - // Clicking the dench workspace root → restore full workspace mode + // Clicking the workspace root → restore full workspace mode if (workspaceRoot && node.path === workspaceRoot) { setBrowseDir(null); return; diff --git a/apps/web/lib/workspace.ts b/apps/web/lib/workspace.ts index 031cee8fa92..42af5a8244e 100644 --- a/apps/web/lib/workspace.ts +++ b/apps/web/lib/workspace.ts @@ -4,16 +4,14 @@ import { join, resolve, normalize, relative } from "node:path"; import { homedir } from "node:os"; /** - * Resolve the dench workspace directory, checking in order: - * 1. DENCH_WORKSPACE env var - * 2. ~/.openclaw/workspace/dench/ - * 3. ./dench/ (relative to process cwd) + * Resolve the workspace directory, checking in order: + * 1. OPENCLAW_WORKSPACE env var + * 2. ~/.openclaw/workspace/ */ -export function resolveDenchRoot(): string | null { +export function resolveWorkspaceRoot(): string | null { const candidates = [ - process.env.DENCH_WORKSPACE, - join(homedir(), ".openclaw", "workspace", "dench"), - join(process.cwd(), "dench"), + process.env.OPENCLAW_WORKSPACE, + join(homedir(), ".openclaw", "workspace"), ].filter(Boolean) as string[]; for (const dir of candidates) { @@ -22,28 +20,36 @@ export function resolveDenchRoot(): string | null { return null; } +/** @deprecated Use `resolveWorkspaceRoot` instead. */ +export const resolveDenchRoot = resolveWorkspaceRoot; + /** - * Return the workspace path prefix relative to the repo root (agent's cwd). - * Tree paths are relative to the dench workspace root (e.g. "knowledge/leads/foo.md"), - * but the agent runs from the repo root, so it needs "dench/knowledge/leads/foo.md". - * Returns e.g. "dench", or null if the workspace isn't found. + * Return the workspace path prefix for the agent. + * Returns the absolute workspace path (e.g. ~/.openclaw/workspace), + * or a relative path from the repo root if the workspace is inside it. */ export function resolveAgentWorkspacePrefix(): string | null { - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) {return null;} - const cwd = process.cwd(); - const repoRoot = cwd.endsWith(join("apps", "web")) - ? resolve(cwd, "..", "..") - : cwd; + // If the workspace is an absolute path outside the repo, return it as-is + if (root.startsWith("/")) { + const cwd = process.cwd(); + const repoRoot = cwd.endsWith(join("apps", "web")) + ? resolve(cwd, "..", "..") + : cwd; + const rel = relative(repoRoot, root); + // If the relative path starts with "..", it's outside the repo — use absolute + if (rel.startsWith("..")) {return root;} + return rel || root; + } - const rel = relative(repoRoot, root); - return rel || null; + return root; } /** Path to the DuckDB database file, or null if workspace doesn't exist. */ export function duckdbPath(): string | null { - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) {return null;} const dbPath = join(root, "workspace.duckdb"); return existsSync(dbPath) ? dbPath : null; @@ -53,7 +59,7 @@ export function duckdbPath(): string | null { * Resolve the duckdb CLI binary path. * Checks common locations since the Next.js server may have a minimal PATH. */ -function resolveDuckdbBin(): string | null { +export function resolveDuckdbBin(): string | null { const home = homedir(); const candidates = [ // User-local installs @@ -200,14 +206,14 @@ export function duckdbQueryOnFile>( } /** - * Validate and resolve a path within the dench workspace. + * Validate and resolve a path within the workspace. * Prevents path traversal by ensuring the resolved path stays within root. * Returns the absolute path or null if invalid/nonexistent. */ export function safeResolvePath( relativePath: string, ): string | null { - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) {return null;} // Reject obvious traversal attempts @@ -250,7 +256,7 @@ export function parseSimpleYaml( ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) ) { - value = (value as string).slice(1, -1); + value = (value).slice(1, -1); } // Parse booleans and numbers @@ -293,7 +299,7 @@ export function isSystemFile(relativePath: string): boolean { * Still prevents path traversal. */ export function safeResolveNewPath(relativePath: string): string | null { - const root = resolveDenchRoot(); + const root = resolveWorkspaceRoot(); if (!root) {return null;} const normalized = normalize(relativePath); diff --git a/apps/web/public/fonts/Bookerly-Bold.ttf b/apps/web/public/fonts/Bookerly-Bold.ttf deleted file mode 100644 index 4cc9bf07fce..00000000000 Binary files a/apps/web/public/fonts/Bookerly-Bold.ttf and /dev/null differ diff --git a/apps/web/public/fonts/Bookerly-BoldItalic.ttf b/apps/web/public/fonts/Bookerly-BoldItalic.ttf deleted file mode 100644 index 4bd84830340..00000000000 Binary files a/apps/web/public/fonts/Bookerly-BoldItalic.ttf and /dev/null differ diff --git a/apps/web/public/fonts/Bookerly-Regular.ttf b/apps/web/public/fonts/Bookerly-Regular.ttf deleted file mode 100644 index 5b2ccd8d01a..00000000000 Binary files a/apps/web/public/fonts/Bookerly-Regular.ttf and /dev/null differ diff --git a/apps/web/public/fonts/Bookerly-RegularItalic.ttf b/apps/web/public/fonts/Bookerly-RegularItalic.ttf deleted file mode 100644 index 55adbf5ab01..00000000000 Binary files a/apps/web/public/fonts/Bookerly-RegularItalic.ttf and /dev/null differ diff --git a/apps/web/public/fonts/FoundationTitlesHand.ttf b/apps/web/public/fonts/FoundationTitlesHand.ttf deleted file mode 100644 index a6e1bd128ea..00000000000 Binary files a/apps/web/public/fonts/FoundationTitlesHand.ttf and /dev/null differ diff --git a/apps/web/public/fonts/SpaceGrotesk-Bold.ttf b/apps/web/public/fonts/SpaceGrotesk-Bold.ttf deleted file mode 100644 index 0408641c61d..00000000000 Binary files a/apps/web/public/fonts/SpaceGrotesk-Bold.ttf and /dev/null differ diff --git a/apps/web/public/fonts/SpaceGrotesk-Light.ttf b/apps/web/public/fonts/SpaceGrotesk-Light.ttf deleted file mode 100644 index d41bcccd864..00000000000 Binary files a/apps/web/public/fonts/SpaceGrotesk-Light.ttf and /dev/null differ diff --git a/apps/web/public/fonts/SpaceGrotesk-Medium.ttf b/apps/web/public/fonts/SpaceGrotesk-Medium.ttf deleted file mode 100644 index 7d44b663b97..00000000000 Binary files a/apps/web/public/fonts/SpaceGrotesk-Medium.ttf and /dev/null differ diff --git a/apps/web/public/fonts/SpaceGrotesk-Regular.ttf b/apps/web/public/fonts/SpaceGrotesk-Regular.ttf deleted file mode 100644 index 981bcf5b2cb..00000000000 Binary files a/apps/web/public/fonts/SpaceGrotesk-Regular.ttf and /dev/null differ diff --git a/apps/web/public/fonts/SpaceGrotesk-SemiBold.ttf b/apps/web/public/fonts/SpaceGrotesk-SemiBold.ttf deleted file mode 100644 index e7e02e51e48..00000000000 Binary files a/apps/web/public/fonts/SpaceGrotesk-SemiBold.ttf and /dev/null differ diff --git a/skills/dench/SKILL.md b/skills/dench/SKILL.md index 32aff78ea4e..5d1b68b6c8a 100644 --- a/skills/dench/SKILL.md +++ b/skills/dench/SKILL.md @@ -6,15 +6,15 @@ metadata: { "openclaw": { "inject": true, "always": true, "emoji": "📊" } } # Dench Workspace -You manage a Dench workspace stored locally at `dench/` in your working directory. -All structured data lives in **DuckDB** (`dench/workspace.duckdb`). Documents are **markdown files** in `dench/**`. Organization context is in `dench/workspace_context.yaml` (READ-ONLY). +You manage a Dench workspace stored at `~/.openclaw/workspace`. +All structured data lives in **DuckDB** (`~/.openclaw/workspace/workspace.duckdb`). Documents are **markdown files** in `~/.openclaw/workspace/**`. Organization context is in `~/.openclaw/workspace/workspace_context.yaml` (READ-ONLY). -All actions should look into / edit and work on the `dench/**` directory by default unless told otherwise. Exceptions to this are the `SOUL.md`, `skills/`, `memory/`, `USER.md`, `IDENTITY.md`, `TOOLS.md`, `AGENTS.md` and `MEMORY.md` and other such files. +All actions should look into / edit and work on `~/.openclaw/workspace/**` by default unless told otherwise. Exceptions to this are the `SOUL.md`, `skills/`, `memory/`, `USER.md`, `IDENTITY.md`, `TOOLS.md`, `AGENTS.md` and `MEMORY.md` and other such files. ## Workspace Structure ``` -dench/ +~/.openclaw/workspace/ workspace_context.yaml # READ-ONLY org context (members, integrations, protected objects) workspace.duckdb # DuckDB database — sole source of truth for structured data people/ # Object directory @@ -61,20 +61,20 @@ Generate by querying DuckDB then writing the file: ```bash # 1. Query object + fields from DuckDB -duckdb dench/workspace.duckdb -json " +duckdb ~/.openclaw/workspace/workspace.duckdb -json " SELECT o.id, o.name, o.description, o.icon, o.default_view, (SELECT COUNT(*) FROM entries WHERE object_id = o.id) as entry_count FROM objects o WHERE o.name = 'lead' " -duckdb dench/workspace.duckdb -json " +duckdb ~/.openclaw/workspace/workspace.duckdb -json " SELECT name, type, required, enum_values FROM fields WHERE object_id = (SELECT id FROM objects WHERE name = 'lead') ORDER BY sort_order " # 2. Write .object.yaml from the query results -mkdir -p dench/lead -cat > dench/lead/.object.yaml << 'YAML' +mkdir -p ~/.openclaw/workspace/lead +cat > ~/.openclaw/workspace/lead/.object.yaml << 'YAML' id: "AbCdEfGh..." name: "lead" description: "Sales leads tracking" @@ -102,9 +102,9 @@ YAML On every conversation: -1. Read `dench/workspace_context.yaml` for org context, members, integrations, protected objects. **NEVER modify this file.** +1. Read `~/.openclaw/workspace/workspace_context.yaml` for org context, members, integrations, protected objects. **NEVER modify this file.** 2. Install duckdb if it doesn't exist: `curl https://install.duckdb.org | sh` -3. If `dench/workspace.duckdb` does not exist, initialize it with the schema below. +3. If `~/.openclaw/workspace/workspace.duckdb` does not exist, initialize it with the schema below. ## workspace_context.yaml (READ-ONLY) @@ -120,7 +120,7 @@ This file is generated by Dench and synced via S3. It contains: ## DuckDB Schema -Initialize via `exec` with `duckdb dench/workspace.duckdb`: +Initialize via `exec` with `duckdb ~/.openclaw/workspace/workspace.duckdb`: ```sql -- Nanoid 32 macro: generates IDs matching Dench's Supabase nanoid format @@ -243,7 +243,7 @@ SELECT * FROM v_leads WHERE "Email Address" LIKE '%@gmail.com'; ## SQL Operations Reference -All operations use `exec` with `duckdb dench/workspace.duckdb`. Batch related SQL in a single exec call with transactions. +All operations use `exec` with `duckdb ~/.openclaw/workspace/workspace.duckdb`. Batch related SQL in a single exec call with transactions. ### Create Object @@ -328,13 +328,13 @@ DELETE FROM objects WHERE id = '' AND immutable = false; ### Bulk Import from CSV ```sql -COPY entries FROM 'dench/exports/import.csv' (AUTO_DETECT true); +COPY entries FROM '~/.openclaw/workspace/exports/import.csv' (AUTO_DETECT true); ``` ### Export to CSV ```sql -COPY (SELECT * FROM v_leads) TO 'dench/exports/leads.csv' (HEADER true); +COPY (SELECT * FROM v_leads) TO '~/.openclaw/workspace/exports/leads.csv' (HEADER true); ``` ## Full Workflow: Create CRM Structure in One Shot @@ -385,13 +385,13 @@ COMMIT; **Step 2 — Filesystem: Create object directory + .object.yaml** (exec call): ```bash -mkdir -p dench/lead +mkdir -p ~/.openclaw/workspace/lead # Query the object metadata from DuckDB to build .object.yaml -OBJ_ID=$(duckdb dench/workspace.duckdb -noheader -list "SELECT id FROM objects WHERE name = 'lead'") -ENTRY_COUNT=$(duckdb dench/workspace.duckdb -noheader -list "SELECT COUNT(*) FROM entries WHERE object_id = '$OBJ_ID'") +OBJ_ID=$(duckdb ~/.openclaw/workspace/workspace.duckdb -noheader -list "SELECT id FROM objects WHERE name = 'lead'") +ENTRY_COUNT=$(duckdb ~/.openclaw/workspace/workspace.duckdb -noheader -list "SELECT COUNT(*) FROM entries WHERE object_id = '$OBJ_ID'") -cat > dench/lead/.object.yaml << 'YAML' +cat > ~/.openclaw/workspace/lead/.object.yaml << 'YAML' id: "" name: "lead" description: "Sales leads tracking" @@ -424,9 +424,9 @@ YAML ```bash # Verify view works -duckdb dench/workspace.duckdb "SELECT COUNT(*) FROM v_lead" +duckdb ~/.openclaw/workspace/workspace.duckdb "SELECT COUNT(*) FROM v_lead" # Verify .object.yaml exists -cat dench/lead/.object.yaml +cat ~/.openclaw/workspace/lead/.object.yaml ``` ## Kanban Boards @@ -476,8 +476,8 @@ COMMIT; **Step 2 — Filesystem (MANDATORY):** ```bash -mkdir -p dench/task -cat > dench/task/.object.yaml << 'YAML' +mkdir -p ~/.openclaw/workspace/task +cat > ~/.openclaw/workspace/task/.object.yaml << 'YAML' id: "" name: "task" description: "Task tracking board" @@ -493,7 +493,7 @@ fields: YAML ``` -**Step 3 — Verify:** `duckdb dench/workspace.duckdb "SELECT COUNT(*) FROM v_task"` and `cat dench/task/.object.yaml`. +**Step 3 — Verify:** `duckdb ~/.openclaw/workspace/workspace.duckdb "SELECT COUNT(*) FROM v_task"` and `cat ~/.openclaw/workspace/task/.object.yaml`. ## Field Types Reference @@ -555,11 +555,11 @@ YAML ## Document Management -Documents are markdown files in `dench/**`. The DuckDB `documents` table tracks metadata only; the `.md` file IS the content. +Documents are markdown files in `~/.openclaw/workspace/**`. The DuckDB `documents` table tracks metadata only; the `.md` file IS the content. ### Create Document -1. Write the `.md` file: `write dench/projects/roadmap.md` +1. Write the `.md` file: `write ~/.openclaw/workspace/projects/roadmap.md` 2. Insert metadata into DuckDB: ```sql @@ -593,8 +593,8 @@ You MUST complete ALL steps below after ANY schema mutation (create/update/delet ### After creating or modifying an OBJECT or its FIELDS: - [ ] `CREATE OR REPLACE VIEW v_{object_name}` — regenerate the PIVOT view -- [ ] `mkdir -p dench/{object_name}/` — create the object directory -- [ ] Write `dench/{object_name}/.object.yaml` — metadata projection with id, name, description, icon, default_view, entry_count, and full field list +- [ ] `mkdir -p ~/.openclaw/workspace/{object_name}/` — create the object directory +- [ ] Write `~/.openclaw/workspace/{object_name}/.object.yaml` — metadata projection with id, name, description, icon, default_view, entry_count, and full field list - [ ] If object has a `parent_document_id`, place directory inside the parent document's directory - [ ] Update `WORKSPACE.md` if it exists @@ -606,12 +606,12 @@ You MUST complete ALL steps below after ANY schema mutation (create/update/delet ### After deleting an OBJECT: - [ ] `DROP VIEW IF EXISTS v_{object_name}` — remove the view -- [ ] `rm -rf dench/{object_name}/` — remove the directory (unless it contains nested documents that need relocating) +- [ ] `rm -rf ~/.openclaw/workspace/{object_name}/` — remove the directory (unless it contains nested documents that need relocating) - [ ] Update `WORKSPACE.md` ### After creating or modifying a DOCUMENT: -- [ ] Write the `.md` file to the correct path in `dench/**` +- [ ] Write the `.md` file to the correct path in `~/.openclaw/workspace/**` - [ ] `INSERT INTO documents` — ensure metadata row exists with correct `file_path`, `parent_id`, or `parent_object_id` These steps ensure the filesystem always mirrors DuckDB. The sidebar depends on `.object.yaml` files — if they are missing, objects will not appear. @@ -622,7 +622,7 @@ Reports are JSON config files (`.report.json`) that the web app renders as live ### Report file format -Store reports as `.report.json` files in `dench/**` (wherever appropriate / create directories if you need for better structure). The JSON schema: +Store reports as `.report.json` files in `~/.openclaw/workspace/**` (wherever appropriate / create directories if you need for better structure). The JSON schema: ```json { @@ -766,8 +766,8 @@ The user can then "Pin" the inline report to save it as a `.report.json` file. After creating a `.report.json` file: - [ ] Verify the report JSON is valid and all SQL queries work: test each panel's SQL individually -- [ ] Choose which directory the report should be created in the `dench/` workspace based on the context of the conversation, if nothing vert relevant, create/use the `dench/reports/` directory. -- [ ] Write the file: `dench/**/{slug}.report.json` +- [ ] Choose which directory the report should be created in `~/.openclaw/workspace` based on the context of the conversation, if nothing vert relevant, create/use the `~/.openclaw/workspace/reports/` directory. +- [ ] Write the file: `~/.openclaw/workspace/**/{slug}.report.json` - [ ] Tell the user they can view it in the workspace sidebar under whichever directory it was rightfully placed in based on the context. ### Choosing the right chart type @@ -783,7 +783,7 @@ After creating a `.report.json` file: ## Critical Reminders - Handle the ENTIRE CRM operation from analysis to SQL execution to filesystem projection to summary -- **NEVER SKIP FILESYSTEM PROJECTION**: After creating/modifying any object, you MUST create/update `dench/{object}/.object.yaml` AND the `v_{object}` view. If you skip this, the object will be invisible in the sidebar. This is NOT optional. +- **NEVER SKIP FILESYSTEM PROJECTION**: After creating/modifying any object, you MUST create/update `~/.openclaw/workspace/{object}/.object.yaml` AND the `v_{object}` view. If you skip this, the object will be invisible in the sidebar. This is NOT optional. - **THREE STEPS, EVERY TIME**: (1) SQL transaction, (2) filesystem projection (.object.yaml + directory), (3) verify. An operation is NOT complete until all three are done. - Always check existing data before creating (`SELECT` before `INSERT`, or `ON CONFLICT`) - Use views (`v_{object}`) for all reads — never write raw PIVOT queries for search @@ -801,8 +801,8 @@ After creating a `.report.json` file: - **workspace_context.yaml**: READ-ONLY. Never modify. Data flows from Dench UI only. - **Source of truth**: DuckDB for all structured data. Filesystem for document content and navigation tree. Never duplicate entry data to the filesystem. - **ENTRY COUNT**: After adding entries, update `entry_count` in `.object.yaml`. -- **NEVER POLLUTE THE WORKSPACE**: Always keep cleaning / organising the workspace to something more nicely structured. Always look out for bloat and too many random files scattered around everywhere for no reason, every time you do any actions in filesystem always try to come up with the most efficient and nice file system structure for inside the `dench/` workspace. -- **TEMPORARY FILES**: All temporary scripts / code / text / other files as and when needed for processing must go into `dench/tmp/` directory (create it if it doesn't exist, only if needed). +- **NEVER POLLUTE THE WORKSPACE**: Always keep cleaning / organising the workspace to something more nicely structured. Always look out for bloat and too many random files scattered around everywhere for no reason, every time you do any actions in filesystem always try to come up with the most efficient and nice file system structure inside `~/.openclaw/workspace`. +- **TEMPORARY FILES**: All temporary scripts / code / text / other files as and when needed for processing must go into `~/.openclaw/workspace/tmp/` directory (create it if it doesn't exist, only if needed). ## Browser Use