Skip to content

Commit f7a7d26

Browse files
author
Atlas (Engineering Lead)
committed
feat(files): add right-click context menu for file actions
PINE-56: Phase 6 feedback - Part 2 Add file context menu with Download and Delete actions: - Add showFileMenu/onFileAction to electron/preload.ts - IPC handler already existed in main.ts (lines 656-673) - Add onContextMenu handler to FilesPanel file buttons - Delete action shows confirmation dialog, then deletes file - Download action opens signed URL in browser - Add TypeScript types to electron.d.ts Shortcuts in Settings were already implemented (keyboard-shortcuts.ts has 'assistant' and 'chat' categories).
1 parent 8e02a6e commit f7a7d26

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

electron/preload.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
219219
ipcRenderer.on('context-menu:assistant-action', handler)
220220
return () => ipcRenderer.removeListener('context-menu:assistant-action', handler)
221221
},
222+
showFileMenu: (assistantName: string, fileId: string, fileName: string): void => {
223+
ipcRenderer.send('context-menu:show-file', assistantName, fileId, fileName)
224+
},
225+
onFileAction: (callback: (action: { action: string; assistantName: string; fileId: string; fileName: string }) => void): (() => void) => {
226+
const handler = (_event: any, data: { action: string; assistantName: string; fileId: string; fileName: string }) => callback(data)
227+
ipcRenderer.on('context-menu:file-action', handler)
228+
return () => ipcRenderer.removeListener('context-menu:file-action', handler)
229+
},
222230
},
223231
profiles: {
224232
getAll: async (): Promise<ConnectionProfile[]> => {

src/components/files/FilesPanel.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo, useCallback } from 'react'
1+
import { useState, useMemo, useCallback, useEffect } from 'react'
22
import { FileText, Upload, Check, AlertCircle, Loader2, Trash2 } from 'lucide-react'
33
import { usePinecone } from '../../providers/PineconeProvider'
44
import { useAssistantSelection } from '../../context/AssistantSelectionContext'
@@ -119,6 +119,51 @@ export function FilesPanel({ className }: FilesPanelProps) {
119119
setActiveFile(fileId)
120120
}, [setActiveFile])
121121

122+
// Handle right-click context menu on file items
123+
const handleFileContextMenu = useCallback((e: React.MouseEvent, file: AssistantFile) => {
124+
e.preventDefault()
125+
if (!activeAssistant) return
126+
window.electronAPI.contextMenu.showFileMenu(activeAssistant, file.id, file.name)
127+
}, [activeAssistant])
128+
129+
// Listen for file context menu actions
130+
useEffect(() => {
131+
if (!currentProfile?.id || !activeAssistant) return
132+
133+
const cleanup = window.electronAPI.contextMenu.onFileAction(async (data) => {
134+
if (data.assistantName !== activeAssistant) return
135+
136+
if (data.action === 'delete') {
137+
// Confirm deletion
138+
const confirmed = window.confirm(`Delete "${data.fileName}"?\n\nThis action cannot be undone.`)
139+
if (!confirmed) return
140+
141+
try {
142+
await window.electronAPI.assistant.files.delete(currentProfile.id, activeAssistant, data.fileId)
143+
// Clear selection if deleted file was selected
144+
if (activeFile === data.fileId) {
145+
setActiveFile(null)
146+
}
147+
refetch()
148+
} catch (err) {
149+
console.error('Failed to delete file:', err)
150+
}
151+
} else if (data.action === 'download') {
152+
// Get file details to get signed URL
153+
try {
154+
const file = await window.electronAPI.assistant.files.describe(currentProfile.id, activeAssistant, data.fileId)
155+
if (file.signedUrl) {
156+
await window.electronAPI.shell.openExternal(file.signedUrl)
157+
}
158+
} catch (err) {
159+
console.error('Failed to download file:', err)
160+
}
161+
}
162+
})
163+
164+
return cleanup
165+
}, [currentProfile?.id, activeAssistant, activeFile, setActiveFile, refetch])
166+
122167
// If no assistant is selected, show prompt
123168
if (!activeAssistant) {
124169
return (
@@ -254,6 +299,7 @@ export function FilesPanel({ className }: FilesPanelProps) {
254299
<button
255300
key={file.id}
256301
onClick={() => handleFileClick(file.id)}
302+
onContextMenu={(e) => handleFileContextMenu(e, file)}
257303
className={`w-full px-3 py-1.5 text-left transition-colors duration-100 mx-1 rounded-md ${
258304
isActive
259305
? 'bg-black/[0.08] dark:bg-white/[0.10]'

src/types/electron.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ declare global {
381381
onNamespaceAction: (callback: (action: { action: string; namespace: string }) => void) => () => void
382382
showAssistantMenu: (assistantName: string) => void
383383
onAssistantAction: (callback: (action: { action: string; assistantName: string }) => void) => () => void
384+
showFileMenu: (assistantName: string, fileId: string, fileName: string) => void
385+
onFileAction: (callback: (action: { action: string; assistantName: string; fileId: string; fileName: string }) => void) => () => void
384386
}
385387
profiles: {
386388
getAll: () => Promise<ConnectionProfile[]>

0 commit comments

Comments
 (0)