From af69bd501728e8bc17d04c2c204ea1c015231478 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 6 Aug 2026 13:34:37 -0700 Subject: [PATCH] [eric] composer: attachment chips answer right-click, file chips gain Copy path and a reveal-only Finder IPC --- electron/main.js | 13 +++++++ electron/preload.js | 2 ++ .../ChatInput/view/AttachmentChips.tsx | 36 +++++++++++++++++++ .../src/app/pages/Dashboard/desktop/chord.ts | 2 +- frontend/src/types/electron.d.ts | 1 + 5 files changed, 53 insertions(+), 1 deletion(-) diff --git a/electron/main.js b/electron/main.js index 869ddfb5..926cee70 100644 --- a/electron/main.js +++ b/electron/main.js @@ -3168,6 +3168,19 @@ ipcMain.handle('get-webview-preload-path', () => { return `file://${path.join(__dirname, 'webview-preload.js')}`; }); +// Reveal a user-attached composer file in Finder/Explorer. Reveal-only on an existing path: +// showItemInFolder never opens or executes the file, so the worst misuse is popping a Finder window. +ipcMain.handle('files:reveal', (event, filePath) => { + try { + const p = path.resolve(String(filePath || '')); + if (!fs.existsSync(p)) return { ok: false }; + shell.showItemInFolder(p); + return { ok: true }; + } catch (_) { + return { ok: false }; + } +}); + // Reveal a diagnostics bundle in the file manager so the user can drag it into a GitHub issue. // Scoped HARD to the backend's diagnostics dir: this must never become an arbitrary-path opener. ipcMain.handle('help:reveal-bundle', (event, folderPath) => { diff --git a/electron/preload.js b/electron/preload.js index 82cbf4dd..df9a07ab 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -90,6 +90,8 @@ contextBridge.exposeInMainWorld('openswarm', { }, // Reveal a diagnostics folder in Finder/Explorer (path validated in main; diagnostics dir only). revealBundle: (folderPath) => ipcRenderer.invoke('help:reveal-bundle', folderPath), + // Reveal a user-attached file in Finder/Explorer (reveal-only; main checks existence). + revealPath: (filePath) => ipcRenderer.invoke('files:reveal', filePath), // Native OS notification for a finished workflow run, posted by the MAIN process // so it survives a minimized/hidden/backgrounded renderer (the renderer's own diff --git a/frontend/src/app/pages/AgentChat/ChatInput/view/AttachmentChips.tsx b/frontend/src/app/pages/AgentChat/ChatInput/view/AttachmentChips.tsx index bc209fc8..4542ef37 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/view/AttachmentChips.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput/view/AttachmentChips.tsx @@ -8,6 +8,8 @@ import FolderOpenIcon from '@mui/icons-material/FolderOpen'; import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; import AdsClickIcon from '@mui/icons-material/AdsClick'; import { getToolGroupIcon } from '@/app/components/editor/CommandPicker'; +import { openCardContextMenu } from '@/app/pages/Dashboard/desktop/openCardContextMenu'; +import { IS_MAC } from '@/app/pages/Dashboard/desktop/chord'; import { SelectedElement } from '@/app/components/editor/ElementSelectionContext'; import { ContextPath } from '@/app/components/editor/DirectoryBrowser'; import { ClaudeTokens } from '@/shared/styles/claudeTokens'; @@ -74,6 +76,13 @@ export const AttachmentChips: React.FC = ({ '&:hover': { opacity: 0.85, transform: 'scale(1.04)' }, }} onClick={() => setLightboxSrc(img.preview)} + onContextMenu={(e) => openCardContextMenu(e, { + items: [ + { label: 'View', onClick: () => setLightboxSrc(img.preview) }, + { kind: 'separator' }, + { label: 'Remove', danger: true, onClick: () => removeImage(idx) }, + ], + })} > = ({ setTimeout(() => setCopiedPathIdx((cur) => cur === idx ? null : cur), 1200); }} onDelete={() => setContextPaths((prev) => prev.filter((_, i) => i !== idx))} + onContextMenu={(e) => openCardContextMenu(e, { + items: [ + { + label: 'Copy path', + onClick: () => { + navigator.clipboard.writeText(cp.path); + setCopiedPathIdx(idx); + setTimeout(() => setCopiedPathIdx((cur) => cur === idx ? null : cur), 1200); + }, + }, + { label: IS_MAC ? 'Reveal in Finder' : 'Show in Explorer', onClick: () => { void window.openswarm?.revealPath?.(cp.path); } }, + { kind: 'separator' }, + { label: 'Remove', danger: true, onClick: () => setContextPaths((prev) => prev.filter((_, i) => i !== idx)) }, + ], + })} sx={{ bgcolor: `${chipColor}12`, color: chipColor, @@ -184,6 +208,11 @@ export const AttachmentChips: React.FC = ({ label={`@${ft.label.toLowerCase()}`} size="small" onDelete={() => setForcedTools((prev) => prev.filter((_, i) => i !== idx))} + onContextMenu={(e) => openCardContextMenu(e, { + items: [ + { label: 'Remove', danger: true, onClick: () => setForcedTools((prev) => prev.filter((_, i) => i !== idx)) }, + ], + })} sx={{ bgcolor: `${c.status.info}15`, color: c.status.info, @@ -236,6 +265,13 @@ export const AttachmentChips: React.FC = ({ label={chipLabel} size="small" onDelete={() => elementSelection?.removeOwnerElement(ownerId, el.id)} + onContextMenu={(e) => openCardContextMenu(e, { + items: [ + { label: 'Copy selector', onClick: () => { navigator.clipboard.writeText(el.selectorPath); } }, + { kind: 'separator' }, + { label: 'Remove', danger: true, onClick: () => elementSelection?.removeOwnerElement(ownerId, el.id) }, + ], + })} sx={{ bgcolor: 'rgba(59, 130, 246, 0.1)', color: '#3b82f6', diff --git a/frontend/src/app/pages/Dashboard/desktop/chord.ts b/frontend/src/app/pages/Dashboard/desktop/chord.ts index 4a6892a2..f1f41d9b 100644 --- a/frontend/src/app/pages/Dashboard/desktop/chord.ts +++ b/frontend/src/app/pages/Dashboard/desktop/chord.ts @@ -1,5 +1,5 @@ // Renders a shortcut the way the platform writes it: mac glyphs run together, Windows spells them with +. -const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/i.test(navigator.platform); +export const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/i.test(navigator.platform); const GLYPH: Record = { mod: IS_MAC ? '⌘' : 'Ctrl', diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index cecea20d..d2d3a058 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -101,6 +101,7 @@ declare global { onOauthClaim?: (cb: (url: string) => void) => () => void; notify?: (payload: OpenSwarmNotifyRequest) => Promise; onNotificationAction?: (cb: (payload: OpenSwarmNotifyAction) => void) => () => void; + revealPath?: (filePath: string) => Promise<{ ok: boolean }>; } interface Window {