[eric] desktop: dock gains the og toolbar actions (chat/browser/workflow/note/history); Applications no longer kills the app (getFileIcon native throw SIGTRAPped main; icons now decode via defaults+sips children)

This commit is contained in:
ciregenz
2026-07-20 18:36:00 -07:00
parent 4a1eaee3a7
commit 9362e0fe3e
4 changed files with 83 additions and 3 deletions
+30 -2
View File
@@ -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 (_) {
@@ -339,8 +339,14 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
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(() => {
@@ -263,6 +263,9 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
onHighlightCard?.(cardId);
}}
onApplications={() => setAppsWindowOpen((v) => !v)}
onNewAgent={onNewAgent}
onAddBrowser={onAddBrowser}
onAddNote={onAddNote}
/>
)}
@@ -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 && (
<Box sx={{ width: TILE - 8, height: '1px', background: 'rgba(255,255,255,0.14)' }} />
)}
{/* The og toolbar's actions, dock-resident: chat, browser, workflow, note, history. */}
{([
{ label: 'New chat', icon: <ChatBubbleOutlineIcon sx={{ fontSize: 16, color: '#e8e8ee' }} />, act: onNewAgent },
{ label: 'New browser', icon: <LanguageIcon sx={{ fontSize: 17, color: '#e8e8ee' }} />, act: onAddBrowser },
{ label: 'Workflows', icon: <EventRepeatIcon sx={{ fontSize: 16, color: '#e8e8ee' }} />, act: () => dispatch(openWorkflowsApp()) },
{ label: 'New note', icon: <StickyNote2OutlinedIcon sx={{ fontSize: 16, color: '#e8e8ee' }} />, act: onAddNote },
{ label: 'History', icon: <HistoryIcon sx={{ fontSize: 17, color: '#e8e8ee' }} />, act: () => window.dispatchEvent(new CustomEvent('openswarm:open-history')) },
] as const).map((a) => (
<Tooltip key={a.label} title={a.label} placement="right">
<Box
onClick={a.act}
onMouseEnter={endHover}
sx={{
width: TILE,
height: TILE,
borderRadius: '9px',
background: 'linear-gradient(135deg, #5a5a62, #34343c)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
flexShrink: 0,
transition: 'transform 0.15s ease',
'&:hover': { transform: 'scale(1.12)' },
}}
>
{a.icon}
</Box>
</Tooltip>
))}
<Box sx={{ width: TILE - 8, height: '1px', background: 'rgba(255,255,255,0.14)' }} />
<Box
onClick={() => dispatch(openSettingsModal(undefined))}
onMouseEnter={endHover}