👌 IMPROVE: show all workspace files
This commit is contained in:
parent
ae739514e3
commit
18fab85ae7
@ -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);}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -93,7 +93,6 @@ export function DocumentView({
|
||||
tree={tree ?? []}
|
||||
onSave={onSave}
|
||||
onNavigate={onNavigate}
|
||||
onSwitchToRead={() => setEditMode(false)}
|
||||
searchFn={searchFn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user