diff --git a/electron/main.js b/electron/main.js index 1303894e..2c23248f 100644 --- a/electron/main.js +++ b/electron/main.js @@ -3097,8 +3097,36 @@ ipcMain.handle('get-app-icon', async (_event, name) => { if (!target) return null; if (appIconCache.has(name)) return appIconCache.get(name); try { - const icon = await app.getFileIcon(target, { size: 'large' }); - const dataUrl = icon && !icon.isEmpty() ? icon.toDataURL() : null; + let dataUrl = null; + if (process.platform === 'darwin') { + // NEVER app.getFileIcon here: a corrupt .icns raises a native ObjC exception no JS try/catch + // can contain and SIGTRAPs the whole app (reproduced 2026-07-20: last IPC get-app-icon, + // crashpad in_range_cast warning, death). sips does the decode in a disposable child instead. + const { execFile } = require('child_process'); + const os = require('os'); + const run = (cmd, args) => new Promise((resolve, reject) => { + execFile(cmd, args, { timeout: 5000 }, (err, stdout) => (err ? reject(err) : resolve(String(stdout).trim()))); + }); + const resources = path.join(target, 'Contents', 'Resources'); + let icnsName = await run('/usr/bin/defaults', ['read', path.join(target, 'Contents', 'Info'), 'CFBundleIconFile']).catch(() => ''); + if (icnsName && !icnsName.endsWith('.icns')) icnsName += '.icns'; + let icns = icnsName ? path.join(resources, icnsName) : ''; + if (!icns || !fs.existsSync(icns)) { + const alt = fs.existsSync(resources) ? fs.readdirSync(resources).find((f) => f.endsWith('.icns')) : null; + icns = alt ? path.join(resources, alt) : ''; + } + if (icns && fs.existsSync(icns)) { + const outPng = path.join(os.tmpdir(), `osw-icon-${process.pid}-${Date.now()}.png`); + await run('/usr/bin/sips', ['-s', 'format', 'png', '-z', '128', '128', icns, '--out', outPng]).catch(() => ''); + if (fs.existsSync(outPng)) { + dataUrl = `data:image/png;base64,${fs.readFileSync(outPng).toString('base64')}`; + fs.rmSync(outPng, { force: true }); + } + } + } else { + const icon = await app.getFileIcon(target, { size: 'large' }); + dataUrl = icon && !icon.isEmpty() ? icon.toDataURL() : null; + } appIconCache.set(name, dataUrl); return dataUrl; } catch (_) { diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index b65e55ea..92c4558a 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -339,8 +339,14 @@ const DashboardToolbar = React.forwardRef( onAddBrowser(); } }; + // The desktop dock's History tile opens the same popover as Cmd+O. + const handleOpenHistoryEvent = () => handleOpenHistory(); window.addEventListener('keydown', handleKey); - return () => window.removeEventListener('keydown', handleKey); + window.addEventListener('openswarm:open-history', handleOpenHistoryEvent); + return () => { + window.removeEventListener('keydown', handleKey); + window.removeEventListener('openswarm:open-history', handleOpenHistoryEvent); + }; }, [handleOpenViewPicker, handleOpenHistory, onAddBrowser]); useEffect(() => { diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx index d71edb65..c36e2bec 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx @@ -263,6 +263,9 @@ const DashboardCanvas: React.FC = ({ onHighlightCard?.(cardId); }} onApplications={() => setAppsWindowOpen((v) => !v)} + onNewAgent={onNewAgent} + onAddBrowser={onAddBrowser} + onAddNote={onAddNote} /> )} diff --git a/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx b/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx index 8bc49405..364abac4 100644 --- a/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/DesktopDock.tsx @@ -1,8 +1,14 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import LanguageIcon from '@mui/icons-material/Language'; +import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'; +import EventRepeatIcon from '@mui/icons-material/EventRepeat'; +import StickyNote2OutlinedIcon from '@mui/icons-material/StickyNote2Outlined'; +import HistoryIcon from '@mui/icons-material/History'; import DashboardGlyph from '../canvas/DashboardGlyph'; +import { openWorkflowsApp } from '@/shared/state/dashboardLayoutSlice'; import SettingsIcon from '@mui/icons-material/Settings'; import AppsRoundedIcon from '@mui/icons-material/AppsRounded'; import EditNoteIcon from '@mui/icons-material/EditNote'; @@ -51,6 +57,9 @@ interface DesktopDockProps { selectedIds: string[]; onFocusCard: (id: string, rect: CardRect) => void; onApplications: () => void; + onNewAgent: () => void; + onAddBrowser: () => void; + onAddNote: () => void; } const TILE = 30; @@ -83,6 +92,9 @@ function DesktopDock({ selectedIds, onFocusCard, onApplications, + onNewAgent, + onAddBrowser, + onAddNote, }: DesktopDockProps): React.ReactElement | null { const dispatch = useAppDispatch(); const [hovered, setHovered] = useState<{ id: string; top: number } | null>(null); @@ -250,6 +262,37 @@ function DesktopDock({ {entries.length > 0 && ( )} + {/* The og toolbar's actions, dock-resident: chat, browser, workflow, note, history. */} + {([ + { label: 'New chat', icon: , act: onNewAgent }, + { label: 'New browser', icon: , act: onAddBrowser }, + { label: 'Workflows', icon: , act: () => dispatch(openWorkflowsApp()) }, + { label: 'New note', icon: , act: onAddNote }, + { label: 'History', icon: , act: () => window.dispatchEvent(new CustomEvent('openswarm:open-history')) }, + ] as const).map((a) => ( + + + {a.icon} + + + ))} + dispatch(openSettingsModal(undefined))} onMouseEnter={endHover}