diff --git a/main.js b/main.js index 8d88d5da..8f688cdc 100644 --- a/main.js +++ b/main.js @@ -1023,6 +1023,23 @@ ipcMain.handle('read-work-file', (_event, filePath) => { } }); +// --- IPC: delete-work-file --- +ipcMain.handle('delete-work-file', (_event, filePath) => { + try { + const resolved = path.resolve(filePath); + if (!resolved.includes('/.work-files/') && !resolved.includes('\\.work-files\\')) { + return { ok: false, error: 'access denied' }; + } + if (!fs.existsSync(resolved)) return { ok: false, error: 'not found' }; + fs.unlinkSync(resolved); + // FTS entry is cleaned up on the next get-work-files call (full type rebuild) + return { ok: true }; + } catch (err) { + console.error('Error deleting work file:', err); + return { ok: false, error: err.message }; + } +}); + // --- IPC: search --- ipcMain.handle('search', (_event, type, query, titleOnly) => { return searchByType(type, query, 50, !!titleOnly); diff --git a/preload.js b/preload.js index 4918d621..c5163363 100644 --- a/preload.js +++ b/preload.js @@ -14,6 +14,7 @@ contextBridge.exposeInMainWorld('api', { saveMemory: (filePath, content) => ipcRenderer.invoke('save-memory', filePath, content), getWorkFiles: () => ipcRenderer.invoke('get-work-files'), readWorkFile: (filePath) => ipcRenderer.invoke('read-work-file', filePath), + deleteWorkFile: (filePath) => ipcRenderer.invoke('delete-work-file', filePath), getProjects: (showArchived) => ipcRenderer.invoke('get-projects', showArchived), rebuildCache: () => ipcRenderer.invoke('rebuild-cache'), getActiveSessions: () => ipcRenderer.invoke('get-active-sessions'), diff --git a/public/app.js b/public/app.js index 6ac405c1..449379c2 100644 --- a/public/app.js +++ b/public/app.js @@ -41,6 +41,19 @@ const workFilesViewer = document.getElementById('work-files-viewer'); const workFilesPanel = new ViewerPanel(workFilesViewer, { copyPath: true, copyContent: true, language: 'auto', storageKey: 'workFilesPreviewMode', + format: true, + onDelete: async (filePath) => { + const result = await window.api.deleteWorkFile(filePath); + if (result && result.ok) { + // Hide the panel and reload the list + workFilesViewer.style.display = 'none'; + if (typeof loadWorkFiles === 'function') loadWorkFiles(); + } + return result; + }, + onClose: () => { + workFilesViewer.style.display = 'none'; + }, }); const terminalArea = document.getElementById('terminal-area'); const settingsViewer = document.getElementById('settings-viewer'); diff --git a/public/style.css b/public/style.css index 459f6f14..7fd33f04 100644 --- a/public/style.css +++ b/public/style.css @@ -3905,6 +3905,12 @@ body { display: flex; flex-direction: column; } background: rgba(224,80,112,0.06); } +.fp-delete-btn:hover { + border-color: rgba(224,80,112,0.45); + color: #e05070; + background: rgba(224,80,112,0.10); +} + #file-panel-content { flex: 1; display: flex; diff --git a/public/viewer-panel.js b/public/viewer-panel.js index c123f97e..e3a68c83 100644 --- a/public/viewer-panel.js +++ b/public/viewer-panel.js @@ -45,6 +45,8 @@ class ViewerPanel { preview: true, wrap: true, gotoLine: true, + format: !!opts.format, + delete: !!opts.onDelete, save: !!opts.onSave, close: !!opts.onClose, }); @@ -122,6 +124,57 @@ class ViewerPanel { toolbar.flashCopyContent(); }); } + + if (toolbar.formatBtn) { + toolbar.formatBtn.addEventListener('click', () => this._format()); + } + + if (toolbar.deleteBtn && opts.onDelete) { + toolbar.deleteBtn.addEventListener('click', () => this._delete()); + } + } + + _format() { + if (!this.editorView || !this.filePath) return; + const ext = this.filePath.split('.').pop()?.toLowerCase(); + const raw = this.getContent(); + let formatted = null; + try { + if (ext === 'jsonl') { + // Pretty-print each JSON line, separate with --- to preserve line semantics + const lines = raw.split('\n').filter(l => l.trim().length > 0); + formatted = lines.map(l => JSON.stringify(JSON.parse(l), null, 2)).join('\n---\n'); + } else { + // Default: treat as JSON + formatted = JSON.stringify(JSON.parse(raw), null, 2); + } + } catch (err) { + window.flashButtonText?.(this.toolbar.formatBtn, '!', 1200); + return; + } + if (formatted === raw) return; + this.editorView.dispatch({ + changes: { from: 0, to: this.editorView.state.doc.length, insert: formatted }, + }); + window.flashButtonText?.(this.toolbar.formatBtn, '✓', 800); + } + + async _delete() { + if (!this.opts.onDelete || !this.filePath) return; + // Confirm via native-ish browser confirm + const name = this.filePath.split('/').pop(); + if (!window.confirm(`Delete "${name}"?\n\nThis cannot be undone.`)) return; + try { + const result = await this.opts.onDelete(this.filePath); + if (result && result.ok !== false) { + // Close panel and trigger refresh through onClose + if (this.opts.onClose) this.opts.onClose(); + } else { + window.alert(`Delete failed: ${result?.error || 'unknown error'}`); + } + } catch (err) { + window.alert(`Delete failed: ${err.message}`); + } } /** @@ -135,11 +188,16 @@ class ViewerPanel { this.toolbar.setPath(filePath); const isMd = this._isMarkdown(filePath); + const isJsonish = this._isJsonish(filePath); // Show/hide preview button based on file type if (this.toolbar.previewBtn) { this.toolbar.previewBtn.style.display = isMd ? '' : 'none'; } + // Format button: only for .json / .jsonl + if (this.toolbar.formatBtn) { + this.toolbar.formatBtn.style.display = isJsonish ? '' : 'none'; + } // Save preview preference before resetting const wantPreview = isMd && this.opts.storageKey && localStorage.getItem(this.opts.storageKey) === 'true'; @@ -295,6 +353,12 @@ class ViewerPanel { const ext = filePath.split('.').pop()?.toLowerCase(); return ext === 'md' || ext === 'mdx'; } + + _isJsonish(filePath) { + if (!filePath) return false; + const ext = filePath.split('.').pop()?.toLowerCase(); + return ext === 'json' || ext === 'jsonl'; + } } window.ViewerPanel = ViewerPanel; diff --git a/public/viewer-toolbar.js b/public/viewer-toolbar.js index 8c5b0112..e275f6a5 100644 --- a/public/viewer-toolbar.js +++ b/public/viewer-toolbar.js @@ -17,6 +17,10 @@ const GOTO_LINE_ICON = '