diff --git a/web/src/components/layout/WorkspaceShell.tsx b/web/src/components/layout/WorkspaceShell.tsx
index 160f92e..fdc8062 100644
--- a/web/src/components/layout/WorkspaceShell.tsx
+++ b/web/src/components/layout/WorkspaceShell.tsx
@@ -8,23 +8,33 @@ import { TerminalPanel } from "@/components/terminal/TerminalPanel";
import { FileExplorerPanel } from "@/components/workspace/FileExplorerPanel";
import { StatusBar } from "@/components/workspace/StatusBar";
import { WorkspaceSelector } from "@/components/workspace/WorkspaceSelector";
+import { inferLanguage } from "@/lib/api/files";
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
-import {
- findFileByPath,
- type MockFileNode,
-} from "@/pages/workspace/mock-data";
+import type { MockFileNode } from "@/pages/workspace/mock-data";
interface WorkspaceShellProps {
- files: MockFileNode[];
+ workspaceId: string;
}
-export function WorkspaceShell({ files }: WorkspaceShellProps) {
+function fileNodeFromPath(path: string): MockFileNode {
+ const name = path.split("/").pop() ?? path;
+ return {
+ name,
+ path,
+ kind: "file",
+ language: inferLanguage(name),
+ };
+}
+
+export function WorkspaceShell({ workspaceId }: WorkspaceShellProps) {
const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath);
const sidebarCollapsed = useWorkspaceUiStore((s) => s.sidebarCollapsed);
const terminalVisible = useWorkspaceUiStore((s) => s.terminalVisible);
const toggleSidebar = useWorkspaceUiStore((s) => s.toggleSidebar);
const toggleTerminal = useWorkspaceUiStore((s) => s.toggleTerminal);
- const activeFile = findFileByPath(files, activeFilePath);
+ const activeFile = activeFilePath
+ ? fileNodeFromPath(activeFilePath)
+ : undefined;
return (
@@ -65,7 +75,7 @@ export function WorkspaceShell({ files }: WorkspaceShellProps) {
{!sidebarCollapsed && (
<>
-
+
>
diff --git a/web/src/components/workspace/FileExplorerPanel.tsx b/web/src/components/workspace/FileExplorerPanel.tsx
index 809a0c2..d5fcea6 100644
--- a/web/src/components/workspace/FileExplorerPanel.tsx
+++ b/web/src/components/workspace/FileExplorerPanel.tsx
@@ -1,78 +1,155 @@
import { ChevronRight, FileCode2, Folder } from "lucide-react";
+import type { ReactNode } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
+import { useFileList, type FileEntry } from "@/lib/api/files";
+import { useFileTreeStore } from "@/lib/store/file-tree-store";
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
import { cn } from "@/lib/utils";
-import type { MockFileNode } from "@/pages/workspace/mock-data";
interface FileExplorerPanelProps {
- files: MockFileNode[];
+ workspaceId: string | null;
}
-interface FileTreeNodeProps {
- node: MockFileNode;
+interface FileTreeDirectoryProps {
+ workspaceId: string;
+ entry: FileEntry;
depth: number;
- activeFilePath: string;
onSelectFile: (path: string) => void;
}
-function FileTreeNode({
- node,
- depth,
- activeFilePath,
- onSelectFile,
-}: FileTreeNodeProps) {
- const isFile = node.kind === "file";
- const isActive = isFile && node.path === activeFilePath;
+interface FileTreeFileProps {
+ entry: FileEntry;
+ depth: number;
+ onSelectFile: (path: string) => void;
+}
+
+function FileTreeFile({ entry, depth, onSelectFile }: FileTreeFileProps) {
+ const selectedPath = useFileTreeStore((s) => s.selectedPath);
+ const isSelected = selectedPath === entry.path;
const paddingLeft = `${depth * 0.875 + 0.75}rem`;
return (
- {node.children?.length ? (
-
- {node.children.map((child) => (
-
- ))}
-
- ) : null}
);
}
-export function FileExplorerPanel({ files }: FileExplorerPanelProps) {
- const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath);
+function FileTreeDirectory({
+ workspaceId,
+ entry,
+ depth,
+ onSelectFile,
+}: FileTreeDirectoryProps) {
+ const expanded = useFileTreeStore((s) => s.expandedPaths.has(entry.path));
+ const toggleExpanded = useFileTreeStore((s) => s.toggleExpanded);
+ const { data, isLoading, error } = useFileList(
+ workspaceId,
+ entry.path,
+ expanded,
+ );
+ const paddingLeft = `${depth * 0.875 + 0.75}rem`;
+
+ return (
+
+
+ {expanded && (
+
+ {isLoading && (
+ -
+ Loading…
+
+ )}
+ {error && (
+ -
+ {error.message}
+
+ )}
+ {data?.map((child) =>
+ child.isDir ? (
+
+ ) : (
+
+ ),
+ )}
+
+ )}
+
+ );
+}
+
+function EmptyState({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function FileExplorerPanel({ workspaceId }: FileExplorerPanelProps) {
const setActiveFilePath = useWorkspaceUiStore((s) => s.setActiveFilePath);
+ const setSelectedPath = useFileTreeStore((s) => s.setSelectedPath);
+ const { data, isLoading, error } = useFileList(workspaceId, ".");
+
+ const onSelectFile = (path: string) => {
+ setSelectedPath(path);
+ setActiveFilePath(path);
+ };
+
+ if (!workspaceId) {
+ return (
+
+ );
+ }
return (