From fbbf13eee26100959a0540a9f2945a45742b2a09 Mon Sep 17 00:00:00 2001 From: Vonder Date: Wed, 25 Mar 2026 22:30:43 +0100 Subject: [PATCH 1/7] feat(vault): add local file editing and terminal split view - Add readLocalFile/writeLocalFile IPC handlers for reading/writing local files - Add binary file detection to prevent opening non-text files - Add 'Open File' button in Vault to edit local files directly - Add file editor split view in terminal panels (vertical layout) - FileEditorPanel component with write/preview tabs and Cmd+S save - Add error handling around vault DB initialization in main.ts --- electron/handlers/vault-handlers.ts | 45 ++++++++ electron/main.ts | 14 ++- electron/preload.ts | 4 + package-lock.json | 4 +- .../components/FileEditorPanel.tsx | 106 ++++++++++++++++++ .../TerminalsView/components/TerminalGrid.tsx | 17 +++ .../components/TerminalPanel.tsx | 45 +++++++- .../components/TerminalPanelHeader.tsx | 13 +++ src/components/TerminalsView/index.tsx | 68 +++++++++++ .../VaultView/components/DocumentEditor.tsx | 51 +++++---- src/components/VaultView/index.tsx | 80 ++++++++++++- src/types/electron.d.ts | 2 + 12 files changed, 418 insertions(+), 31 deletions(-) create mode 100644 src/components/TerminalsView/components/FileEditorPanel.tsx diff --git a/electron/handlers/vault-handlers.ts b/electron/handlers/vault-handlers.ts index d7fa9f94..82749f24 100644 --- a/electron/handlers/vault-handlers.ts +++ b/electron/handlers/vault-handlers.ts @@ -341,4 +341,49 @@ export function registerVaultHandlers(deps: VaultHandlerDependencies): void { return { success: false, error: String(err) }; } }); + + // Read a local file from disk + ipcMain.handle('vault:readLocalFile', async (_event, filePath: string) => { + try { + const resolvedPath = path.resolve(filePath); + if (!fs.existsSync(resolvedPath)) { + return { error: 'File not found' }; + } + const stats = fs.statSync(resolvedPath); + if (stats.isDirectory()) { + return { error: 'Path is a directory' }; + } + // Limit to 10MB + if (stats.size > 10 * 1024 * 1024) { + return { error: 'File too large (max 10MB)' }; + } + // Check for binary content by reading a small buffer + const buffer = Buffer.alloc(Math.min(8192, stats.size)); + const fd = fs.openSync(resolvedPath, 'r'); + fs.readSync(fd, buffer, 0, buffer.length, 0); + fs.closeSync(fd); + // Check for null bytes (binary indicator) + if (buffer.includes(0)) { + return { error: 'This file appears to be binary and cannot be edited as text. Only text-based files (.md, .txt, .json, .ts, .js, etc.) are supported.' }; + } + const content = fs.readFileSync(resolvedPath, 'utf-8'); + const filename = path.basename(resolvedPath); + return { content, filename, filePath: resolvedPath }; + } catch (err) { + console.error('Failed to read local file:', err); + return { error: String(err) }; + } + }); + + // Write content back to a local file on disk + ipcMain.handle('vault:writeLocalFile', async (_event, params: { filePath: string; content: string }) => { + try { + const resolvedPath = path.resolve(params.filePath); + fs.writeFileSync(resolvedPath, params.content, 'utf-8'); + return { success: true, filePath: resolvedPath }; + } catch (err) { + console.error('Failed to write local file:', err); + return { success: false, error: String(err) }; + } + }); } diff --git a/electron/main.ts b/electron/main.ts index 231bc4f5..356dab8d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -394,10 +394,20 @@ app.whenReady().then(async () => { }); // Initialize vault database - initVaultDb(); + try { + initVaultDb(); + console.log('[Dorothy] Vault database initialized successfully'); + } catch (err) { + console.error('[Dorothy] Failed to initialize vault database:', err); + } // Register vault handlers - registerVaultHandlers({ getMainWindow }); + try { + registerVaultHandlers({ getMainWindow }); + console.log('[Dorothy] Vault handlers registered successfully'); + } catch (err) { + console.error('[Dorothy] Failed to register vault handlers:', err); + } // Register world (generative zone) handlers registerWorldHandlers({ getMainWindow }); diff --git a/electron/preload.ts b/electron/preload.ts index 84866ad0..5ee8d278 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -508,6 +508,10 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('vault:deleteFolder', params), attachFile: (params: { document_id: string; file_path: string }) => ipcRenderer.invoke('vault:attachFile', params), + readLocalFile: (filePath: string) => + ipcRenderer.invoke('vault:readLocalFile', filePath) as Promise<{ content?: string; filename?: string; filePath?: string; error?: string }>, + writeLocalFile: (params: { filePath: string; content: string }) => + ipcRenderer.invoke('vault:writeLocalFile', params) as Promise<{ success?: boolean; filePath?: string; error?: string }>, // Event listeners onDocumentCreated: (callback: (doc: unknown) => void) => { const listener = (_: unknown, doc: unknown) => callback(doc); diff --git a/package-lock.json b/package-lock.json index 0a78fff6..2cedabb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dorothy", - "version": "1.2.1", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dorothy", - "version": "1.2.1", + "version": "1.2.5", "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@dnd-kit/core": "^6.3.1", diff --git a/src/components/TerminalsView/components/FileEditorPanel.tsx b/src/components/TerminalsView/components/FileEditorPanel.tsx new file mode 100644 index 00000000..400cd8a0 --- /dev/null +++ b/src/components/TerminalsView/components/FileEditorPanel.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { useState, useRef, useCallback } from 'react'; +import { Save, X, Eye, Pencil } from 'lucide-react'; + +interface FileEditorPanelProps { + filePath: string; + filename: string; + content: string; + onSave: (content: string) => void; + onClose: () => void; +} + +type EditorTab = 'write' | 'preview'; + +export default function FileEditorPanel({ filePath, filename, content: initialContent, onSave, onClose }: FileEditorPanelProps) { + const [content, setContent] = useState(initialContent); + const [activeTab, setActiveTab] = useState('write'); + const [saved, setSaved] = useState(true); + const textareaRef = useRef(null); + + const handleChange = useCallback((value: string) => { + setContent(value); + setSaved(false); + }, []); + + const handleSave = useCallback(() => { + onSave(content); + setSaved(true); + }, [content, onSave]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 's') { + e.preventDefault(); + handleSave(); + } + }, [handleSave]); + + return ( +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + {/* Header */} +
+ + {filename} + + {!saved && ( + modified + )} + + {/* Tab toggle */} +
+ + +
+ + + +
+ + {/* Content */} + {activeTab === 'write' ? ( +