From d1e77f52f17cbcc23c01868abc86c476592bd544 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Fri, 15 May 2026 17:50:24 -0400 Subject: [PATCH] new file create --- .../frontend/src/components/FileTree.tsx | 52 ++++-- .../frontend/src/components/FilesPanel.tsx | 169 +++++++++++++++++- 2 files changed, 207 insertions(+), 14 deletions(-) diff --git a/gadget-code/frontend/src/components/FileTree.tsx b/gadget-code/frontend/src/components/FileTree.tsx index c9857c6..794bd66 100644 --- a/gadget-code/frontend/src/components/FileTree.tsx +++ b/gadget-code/frontend/src/components/FileTree.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react'; import { socketClient } from '../lib/socket'; import { WorkspaceMode } from '../lib/types'; import FileTreeNode from './FileTreeNode'; @@ -12,6 +12,10 @@ export interface FileTreeEntry { isHidden?: boolean; } +export interface FileTreeHandle { + refresh: () => void; +} + interface FileTreeProps { workspaceMode: WorkspaceMode; onFileSelect?: (path: string) => void; @@ -24,7 +28,7 @@ interface FileTreeState { errors: Map; } -export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps) { +const FileTree = forwardRef(function FileTree({ workspaceMode, onFileSelect }, ref) { const [state, setState] = useState({ directoryCache: new Map(), expandedPaths: new Set(), @@ -34,22 +38,21 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps) const [rootLoaded, setRootLoaded] = useState(false); - // Load root directory on mount - useEffect(() => { - if (!rootLoaded) { - loadDirectory(''); - setRootLoaded(true); - } - }, []); + // Use refs to hold the latest state so loadDirectory always reads current values + // without needing to be in its dependency array (which causes re-creation loops) + const stateRef = useRef(state); + stateRef.current = state; const loadDirectory = useCallback(async (path: string) => { + const currentState = stateRef.current; + // Don't reload if already cached - if (state.directoryCache.has(path)) { + if (currentState.directoryCache.has(path)) { return; } // Don't reload if already loading - if (state.loadingPaths.has(path)) { + if (currentState.loadingPaths.has(path)) { return; } @@ -110,7 +113,28 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps) }; }); } - }, [state.directoryCache, state.loadingPaths]); + }, []); + + // Load root directory on mount + useEffect(() => { + if (!rootLoaded) { + loadDirectory(''); + setRootLoaded(true); + } + }, [rootLoaded, loadDirectory]); + + // Expose refresh method to parent via ref + useImperativeHandle(ref, () => ({ + refresh: () => { + setState({ + directoryCache: new Map(), + expandedPaths: new Set(), + loadingPaths: new Set(), + errors: new Map(), + }); + setRootLoaded(false); + }, + }), []); const toggleExpand = useCallback((path: string) => { setState(prev => { @@ -215,4 +239,6 @@ export default function FileTree({ workspaceMode, onFileSelect }: FileTreeProps) ); -} +}); + +export default FileTree; diff --git a/gadget-code/frontend/src/components/FilesPanel.tsx b/gadget-code/frontend/src/components/FilesPanel.tsx index c7a087e..8d07358 100644 --- a/gadget-code/frontend/src/components/FilesPanel.tsx +++ b/gadget-code/frontend/src/components/FilesPanel.tsx @@ -1,5 +1,7 @@ +import { useRef, useState, useCallback, useEffect } from "react"; import { WorkspaceMode } from "../lib/types"; -import FileTree from "./FileTree"; +import { socketClient } from "../lib/socket"; +import FileTree, { FileTreeHandle } from "./FileTree"; interface FilesPanelProps { workspaceMode: WorkspaceMode; @@ -10,13 +12,125 @@ export default function FilesPanel({ workspaceMode, onFileSelect }: FilesPanelPr const isReadOnly = workspaceMode === WorkspaceMode.Agent; const isReadWrite = workspaceMode === WorkspaceMode.User; + const fileTreeRef = useRef(null); + + // New file creation state + const [isCreatingFile, setIsCreatingFile] = useState(false); + const [newFilePath, setNewFilePath] = useState(""); + const [isCreating, setIsCreating] = useState(false); + const [createError, setCreateError] = useState(undefined); + const inputRef = useRef(null); + + // Focus the input when the creation UI appears + useEffect(() => { + if (isCreatingFile && inputRef.current) { + inputRef.current.focus(); + } + }, [isCreatingFile]); + + const handleRefresh = useCallback(() => { + fileTreeRef.current?.refresh(); + }, []); + + const handleStartCreate = useCallback(() => { + if (!isReadWrite) return; + setIsCreatingFile(true); + setNewFilePath(""); + setCreateError(undefined); + }, [isReadWrite]); + + const handleCancelCreate = useCallback(() => { + setIsCreatingFile(false); + setNewFilePath(""); + setCreateError(undefined); + }, []); + + const handleConfirmCreate = useCallback(async () => { + const trimmed = newFilePath.trim(); + if (!trimmed) return; + + // Strip leading slash if present (paths are relative to project root) + const normalizedPath = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed; + + setIsCreating(true); + setCreateError(undefined); + + try { + const result = await socketClient.requestFileWrite({ + path: normalizedPath, + content: "", + }); + + if (result.success) { + setIsCreatingFile(false); + setNewFilePath(""); + setCreateError(undefined); + // Refresh the tree to show the new file, then open it in the editor + fileTreeRef.current?.refresh(); + onFileSelect?.(normalizedPath); + } else { + setCreateError(result.error || "Failed to create file"); + } + } catch (err) { + setCreateError(err instanceof Error ? err.message : "Failed to create file"); + } finally { + setIsCreating(false); + } + }, [newFilePath, onFileSelect]); + + const handleInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleConfirmCreate(); + } else if (e.key === "Escape") { + e.preventDefault(); + handleCancelCreate(); + } + }, + [handleConfirmCreate, handleCancelCreate], + ); + return (
+ {/* Header */}

Files

+ {/* New File button */} + + + {/* Refresh button */} + + + {/* RW indicator */} RW + {/* RO indicator */}
+ + {/* New file creation input */} + {isCreatingFile && ( +
+
+ setNewFilePath(e.target.value)} + onKeyDown={handleInputKeyDown} + placeholder="src/utils/helper.ts" + disabled={isCreating} + className="flex-1 bg-bg-primary border border-border-default rounded px-2 py-1 text-sm text-text-primary placeholder-text-muted/50 focus:outline-none focus:border-brand" + /> + {/* Confirm button */} + + {/* Cancel button */} + +
+ {createError && ( +

{createError}

+ )} +
+ )} + + {/* File tree */}
+ + {/* Footer */}

{isReadOnly