diff --git a/apps/web/app/api/workspace/browse-file/route.ts b/apps/web/app/api/workspace/browse-file/route.ts index 3b5b3300ec5..eeed22d9857 100644 --- a/apps/web/app/api/workspace/browse-file/route.ts +++ b/apps/web/app/api/workspace/browse-file/route.ts @@ -18,6 +18,8 @@ const MIME_MAP: Record = { wav: "audio/wav", ogg: "audio/ogg", pdf: "application/pdf", + html: "text/html", + htm: "text/html", }; /** Extensions recognized as code files for syntax-highlighted viewing. */ diff --git a/apps/web/app/api/workspace/raw-file/route.ts b/apps/web/app/api/workspace/raw-file/route.ts index 82861ef8073..25c0084fe13 100644 --- a/apps/web/app/api/workspace/raw-file/route.ts +++ b/apps/web/app/api/workspace/raw-file/route.ts @@ -33,6 +33,8 @@ const MIME_MAP: Record = { m4a: "audio/mp4", // Documents pdf: "application/pdf", + html: "text/html", + htm: "text/html", }; /** diff --git a/apps/web/app/components/sidebar.tsx b/apps/web/app/components/sidebar.tsx index 094683da12f..f4575c955b4 100644 --- a/apps/web/app/components/sidebar.tsx +++ b/apps/web/app/components/sidebar.tsx @@ -437,6 +437,7 @@ export function Sidebar({ setShowCreateWorkspace(true)} + activeProfileHint={String(sidebarRefreshKey)} /> diff --git a/apps/web/app/components/workspace/file-manager-tree.tsx b/apps/web/app/components/workspace/file-manager-tree.tsx index 8f1104f537f..a01694d0dc5 100644 --- a/apps/web/app/components/workspace/file-manager-tree.tsx +++ b/apps/web/app/components/workspace/file-manager-tree.tsx @@ -28,6 +28,8 @@ export type TreeNode = { children?: TreeNode[]; /** When true, the node represents a virtual folder/file outside the real workspace (e.g. Skills, Memories). CRUD ops are disabled. */ virtual?: boolean; + /** True when the entry is a symbolic link / shortcut. */ + symlink?: boolean; }; /** Folder names reserved for virtual sections -- cannot be created/renamed to. */ @@ -153,6 +155,14 @@ function LockBadge() { ); } +function SymlinkBadge() { + return ( + + + + ); +} + function ChevronIcon({ open }: { open: boolean }) { return ( )} + {/* Symlink indicator */} + {node.symlink && !compact && ( + + + + )} + {/* Type badge for objects */} {node.type === "object" && ( void; onCreateWorkspace?: () => void; + /** Parent-tracked active profile -- triggers a re-fetch when it changes (e.g. after workspace creation). */ + activeProfileHint?: string | null; /** When set, this renders instead of the default button; dropdown still opens below. */ trigger?: (props: ProfileSwitcherTriggerProps) => React.ReactNode; }; -export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, trigger }: ProfileSwitcherProps) { +export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, activeProfileHint, trigger }: ProfileSwitcherProps) { const [profiles, setProfiles] = useState([]); const [activeProfile, setActiveProfile] = useState("default"); const [isOpen, setIsOpen] = useState(false); @@ -44,7 +46,7 @@ export function ProfileSwitcher({ onProfileSwitch, onCreateWorkspace, trigger }: useEffect(() => { void fetchProfiles(); - }, [fetchProfiles]); + }, [fetchProfiles, activeProfileHint]); // Close dropdown on outside click useEffect(() => { diff --git a/apps/web/app/components/workspace/workspace-sidebar.tsx b/apps/web/app/components/workspace/workspace-sidebar.tsx index 68610cd9e61..e8db2a1538b 100644 --- a/apps/web/app/components/workspace/workspace-sidebar.tsx +++ b/apps/web/app/components/workspace/workspace-sidebar.tsx @@ -46,6 +46,10 @@ type WorkspaceSidebarProps = { width?: number; /** Called after the user switches to a different profile. */ onProfileSwitch?: () => void; + /** Whether hidden (dot) files/folders are currently shown. */ + showHidden?: boolean; + /** Toggle hidden files visibility. */ + onToggleHidden?: () => void; /** Called when the user clicks the collapse/hide sidebar button. */ onCollapse?: () => void; }; @@ -404,6 +408,8 @@ export function WorkspaceSidebar({ onClose, activeProfile, onProfileSwitch, + showHidden, + onToggleHidden, width: widthProp, onCollapse, }: WorkspaceSidebarProps) { @@ -488,6 +494,7 @@ export function WorkspaceSidebar({ setShowCreateWorkspace(true)} + activeProfileHint={activeProfile} trigger={({ isOpen, onClick, activeProfile: profileName, switching }) => ( + )} + + ); diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index 26eb87c19a8..8aa4ad77a28 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -358,6 +358,7 @@ function WorkspacePageInner() { reconnect: reconnectWorkspace, browseDir, setBrowseDir, parentDir: browseParentDir, workspaceRoot, openclawDir, activeProfile, + showHidden, setShowHidden, } = useWorkspaceWatcher(); // handleProfileSwitch is defined below fetchSessions/fetchCronJobs (avoids TDZ) @@ -1325,6 +1326,8 @@ function WorkspacePageInner() { onExternalDrop={handleSidebarExternalDrop} activeProfile={activeProfile} onProfileSwitch={handleProfileSwitch} + showHidden={showHidden} + onToggleHidden={() => setShowHidden((v) => !v)} mobile onClose={() => setSidebarOpen(false)} /> @@ -1360,6 +1363,8 @@ function WorkspacePageInner() { onExternalDrop={handleSidebarExternalDrop} activeProfile={activeProfile} onProfileSwitch={handleProfileSwitch} + showHidden={showHidden} + onToggleHidden={() => setShowHidden((v) => !v)} width={leftSidebarWidth} onCollapse={() => setLeftSidebarCollapsed(true)} /> diff --git a/skills/dench/SKILL.md b/skills/dench/SKILL.md index bc5d33d863c..39760aec4ef 100644 --- a/skills/dench/SKILL.md +++ b/skills/dench/SKILL.md @@ -667,6 +667,19 @@ VALUES ('Roadmap', 'map', 'projects/roadmap.md', '', 0); - **Field names**: human-readable, proper capitalization ("Email Address" not "email") - **Be descriptive**: "Phone Number" not "Phone" - **Be consistent**: Don't mix "Full Name" and "Name" in the same object +- **TRIPLE ALIGNMENT (MANDATORY)**: The DuckDB object `name`, the filesystem directory name, and the `.object.yaml` `name` field MUST all be identical. If any one of these three diverges, the UI will fail to render the object. For example, if DuckDB has `name = 'contract'`, the directory MUST be `contract/` (in workspace) and the yaml MUST have `name: "contract"`. Never use plural for one and singular for another. + +### Renaming / Moving Objects + +When renaming or relocating an object, you MUST update ALL THREE in a single operation: + +1. **DuckDB**: Update `objects.name` (if FK constraints block this, recreate the object with the new name and migrate entries) +2. **Directory**: `mv` the old directory to the new name +3. **`.object.yaml`**: Update the `name` field to match +4. **PIVOT view**: `DROP VIEW IF EXISTS v_{old_name}; CREATE OR REPLACE VIEW v_{new_name} ...` +5. **Verify**: Confirm all three match and the view returns data + +Never rename partially. If you can't complete all steps, don't start the rename — explain the constraint to the user first. ## Error Handling @@ -872,7 +885,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 `~/.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. +- **NEVER SKIP FILESYSTEM PROJECTION**: After creating/modifying any object, you MUST create/update `{object}/.object.yaml` in workspace 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 @@ -890,8 +903,9 @@ 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 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). +- **NAME CONSISTENCY**: The DuckDB `objects.name`, the filesystem directory name, and `.object.yaml` `name` MUST be identical. A mismatch between ANY of these three will break the UI. Before finishing any object creation or modification, verify: `objects.name == directory_name == yaml.name`. See "Renaming / Moving Objects" under Naming Conventions. +- **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 the workspace. +- **TEMPORARY FILES**: All temporary scripts / code / text / other files as and when needed for processing must go into `tmp/` directory (create it in the workspace if it doesn't exist, only if needed). ## Browser Use