👌 IMPROVE: show all workspace files

This commit is contained in:
kumarabhirup 2026-02-12 09:04:34 -08:00
parent ae739514e3
commit 18fab85ae7
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
5 changed files with 61 additions and 21 deletions

View File

@ -8,7 +8,7 @@ export const runtime = "nodejs";
export type TreeNode = {
name: string;
path: string; // relative to dench/ (or ~skills/, ~memories/ for virtual nodes)
path: string; // relative to dench/ (or ~skills/, ~memories/, ~workspace/ for virtual nodes)
type: "object" | "document" | "folder" | "file" | "database" | "report";
icon?: string;
defaultView?: "table" | "kanban";
@ -214,6 +214,47 @@ 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");
@ -274,6 +315,7 @@ export async function GET() {
if (!root) {
// Even without a dench workspace, return virtual folders if they exist
const tree: TreeNode[] = [];
tree.push(...buildWorkspaceRootFiles());
const skillsFolder = buildSkillsVirtualFolder();
if (skillsFolder) {tree.push(skillsFolder);}
const memoriesFolder = buildMemoriesVirtualFolder();
@ -323,6 +365,10 @@ export async function GET() {
// skip if root unreadable
}
// 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);}

View File

@ -68,6 +68,15 @@ function resolveVirtualPath(virtualPath: string): string | null {
return join(workspaceDir, "memory", rest);
}
if (virtualPath.startsWith("~workspace/")) {
const rest = virtualPath.slice("~workspace/".length);
// Only allow direct filenames (no subdirectories, no traversal)
if (!rest || rest.includes("..") || rest.includes("/")) {
return null;
}
return join(home, ".openclaw", "workspace", rest);
}
return null;
}

View File

@ -93,7 +93,6 @@ export function DocumentView({
tree={tree ?? []}
onSave={onSave}
onNavigate={onNavigate}
onSwitchToRead={() => setEditMode(false)}
searchFn={searchFn}
/>
</div>

View File

@ -618,11 +618,14 @@ export function FileManagerTree({ tree, activePath, onSelect, onRefresh, compact
const containerRef = useRef<HTMLDivElement>(null);
// Auto-expand first level on mount
// Auto-expand first level on mount.
// Keep ~skills and ~memories collapsed by default; always expand ~chats.
const collapsedByDefault = new Set(["~skills", "~memories"]);
useEffect(() => {
if (tree.length > 0 && expandedPaths.size === 0) {
const initial = new Set<string>();
for (const node of tree) {
if (collapsedByDefault.has(node.path)) {continue;}
if (node.children && node.children.length > 0) {
initial.add(node.path);
}

View File

@ -30,8 +30,6 @@ export type MarkdownEditorProps = {
tree: TreeNode[];
onSave?: () => void;
onNavigate?: (path: string) => void;
/** Switch to read-only mode (renders a "Read" button in the top bar). */
onSwitchToRead?: () => void;
/** Optional search function from useSearchIndex for fuzzy @ mention search. */
searchFn?: MentionSearchFn;
};
@ -51,7 +49,6 @@ export function MarkdownEditor({
tree,
onSave,
onNavigate,
onSwitchToRead,
searchFn,
}: MarkdownEditorProps) {
const [saving, setSaving] = useState(false);
@ -347,7 +344,7 @@ export function MarkdownEditor({
return (
<div className="markdown-editor-container">
{/* Sticky top bar: save status + save button + read toggle */}
{/* Sticky top bar: save status + save button */}
<div className="editor-top-bar">
<div className="editor-top-bar-left">
{isDirty && (
@ -378,20 +375,6 @@ export function MarkdownEditor({
>
{saving ? "Saving..." : "Save"}
</button>
{onSwitchToRead && (
<button
type="button"
onClick={onSwitchToRead}
className="editor-mode-toggle"
title="Switch to read mode"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
</svg>
<span>Read</span>
</button>
)}
</div>
</div>