mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-26 06:22:22 +02:00
[eric] desktop: per-card dock glyphs and hues; real OS wallpaper read at runtime (SVG fallback)
This commit is contained in:
@@ -3114,6 +3114,59 @@ ipcMain.handle('open-application', (_event, name) => {
|
||||
return true;
|
||||
});
|
||||
|
||||
// The user's REAL desktop wallpaper for the dashboard background, read at runtime and never
|
||||
// bundled (Apple's images can't ship in an open-source repo). Any failure on any platform
|
||||
// returns null and the renderer keeps its SVG scenery, so this can only ever add.
|
||||
let desktopWallpaperCache;
|
||||
ipcMain.handle('get-desktop-wallpaper', async () => {
|
||||
if (desktopWallpaperCache !== undefined) return desktopWallpaperCache;
|
||||
const { execFile } = require('child_process');
|
||||
const run = (cmd, args) => new Promise((resolve, reject) => {
|
||||
execFile(cmd, args, { timeout: 8000 }, (err, stdout) => (err ? reject(err) : resolve(String(stdout).trim())));
|
||||
});
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
let srcPath = await run('/usr/bin/osascript', ['-e', 'tell application "System Events" to get picture of current desktop']).catch(() => '');
|
||||
if (!srcPath || !fs.existsSync(srcPath)) {
|
||||
// Dynamic/aerial wallpapers report a reaped temp frame; the wallpaper store may still hold a real image path.
|
||||
srcPath = '';
|
||||
try {
|
||||
const storePath = path.join(app.getPath('home'), 'Library', 'Application Support', 'com.apple.wallpaper', 'Store', 'Index.plist');
|
||||
const raw = fs.readFileSync(storePath, 'latin1');
|
||||
const candidates = [...raw.matchAll(/file:\/\/(\/[ -~]+?\.(?:jpg|jpeg|png|heic|tiff))/gi)].map((m) => decodeURIComponent(m[1]));
|
||||
srcPath = candidates.find((c) => fs.existsSync(c)) || '';
|
||||
} catch (_) { /* fall through to the SVG scenery */ }
|
||||
}
|
||||
if (!srcPath) { desktopWallpaperCache = null; return null; }
|
||||
// sips normalizes HEIC sources to jpeg and downscales so the data URL stays small.
|
||||
const outPath = path.join(app.getPath('userData'), 'wallpaper-cache.jpg');
|
||||
await run('/usr/bin/sips', ['-s', 'format', 'jpeg', '--resampleHeightWidthMax', '2400', srcPath, '--out', outPath]);
|
||||
desktopWallpaperCache = `data:image/jpeg;base64,${fs.readFileSync(outPath).toString('base64')}`;
|
||||
return desktopWallpaperCache;
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
// Windows keeps the active wallpaper pre-transcoded as a JPEG; the registry path is the fallback.
|
||||
const transcoded = path.join(app.getPath('appData'), 'Microsoft', 'Windows', 'Themes', 'TranscodedWallpaper');
|
||||
let srcPath = fs.existsSync(transcoded) ? transcoded : null;
|
||||
if (!srcPath) {
|
||||
const out = await run('reg', ['query', 'HKCU\\Control Panel\\Desktop', '/v', 'WallPaper']);
|
||||
const match = out.match(/WallPaper\s+REG_SZ\s+(.+)$/m);
|
||||
if (match && fs.existsSync(match[1].trim())) srcPath = match[1].trim();
|
||||
}
|
||||
if (!srcPath) { desktopWallpaperCache = null; return null; }
|
||||
const ext = path.extname(srcPath).toLowerCase();
|
||||
const mime = ext === '.png' ? 'image/png' : ext === '.bmp' ? 'image/bmp' : 'image/jpeg';
|
||||
desktopWallpaperCache = `data:${mime};base64,${fs.readFileSync(srcPath).toString('base64')}`;
|
||||
return desktopWallpaperCache;
|
||||
}
|
||||
desktopWallpaperCache = null;
|
||||
return null;
|
||||
} catch (_) {
|
||||
desktopWallpaperCache = null;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Affiliate install state. Returns the persisted install.json contents so
|
||||
// the renderer can attach the referral code to authenticated cloud calls
|
||||
// (Stripe checkout, sign-in events) for downstream attribution.
|
||||
|
||||
@@ -77,6 +77,7 @@ contextBridge.exposeInMainWorld('openswarm', {
|
||||
capturePage: (rect) => ipcRenderer.invoke('capture-page', rect),
|
||||
getAppIcon: (name) => ipcRenderer.invoke('get-app-icon', name),
|
||||
openApplication: (name) => ipcRenderer.invoke('open-application', name),
|
||||
getDesktopWallpaper: () => ipcRenderer.invoke('get-desktop-wallpaper'),
|
||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||
getCrashRecoveryInfo: () => ipcRenderer.invoke('get-crash-recovery-info'),
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import DashboardGlyph from '../canvas/DashboardGlyph';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import AppsRoundedIcon from '@mui/icons-material/AppsRounded';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
|
||||
import CoPresentIcon from '@mui/icons-material/CoPresent';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { openSettingsModal } from '@/shared/state/settingsSlice';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
@@ -57,6 +56,21 @@ interface DesktopDockProps {
|
||||
const TILE = 30;
|
||||
const PREVIEW_W = 190;
|
||||
|
||||
// Frames show a colorful per-card dock, not uniform tiles; hues rotate by name so two agents rarely match.
|
||||
const AGENT_TILE_HUES = [
|
||||
'linear-gradient(135deg, #4a7dd6, #2b4fa8)',
|
||||
'linear-gradient(135deg, #8a5bd6, #5b34a8)',
|
||||
'linear-gradient(135deg, #3aa88f, #1f7a64)',
|
||||
'linear-gradient(135deg, #d6754a, #a8492b)',
|
||||
'linear-gradient(135deg, #c94a7d, #96305c)',
|
||||
];
|
||||
|
||||
function hueFor(name: string): string {
|
||||
let h = 0;
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) | 0;
|
||||
return AGENT_TILE_HUES[Math.abs(h) % AGENT_TILE_HUES.length];
|
||||
}
|
||||
|
||||
/** Left-edge desktop dock: one tile per open card, hover previews, click focuses the window. */
|
||||
function DesktopDock({
|
||||
sessions,
|
||||
@@ -80,12 +94,13 @@ function DesktopDock({
|
||||
for (const card of Object.values(cards)) {
|
||||
const session = sessions[card.session_id];
|
||||
if (!session) continue;
|
||||
const title = displayChatTitle(session);
|
||||
list.push({
|
||||
id: card.session_id,
|
||||
label: displayChatTitle(session),
|
||||
label: title,
|
||||
rect: card,
|
||||
tileBg: 'linear-gradient(135deg, #4a7dd6, #2b4fa8)',
|
||||
icon: <AutoAwesomeIcon sx={{ fontSize: 17, color: '#fff' }} />,
|
||||
tileBg: hueFor(title),
|
||||
icon: <DashboardGlyph name={title} size={16} color="#fff" />,
|
||||
snippet: session.turn_label?.label || undefined,
|
||||
});
|
||||
}
|
||||
@@ -103,12 +118,17 @@ function DesktopDock({
|
||||
}
|
||||
for (const [cardKey, vc] of Object.entries(viewCards)) {
|
||||
const output = outputs[vc.output_id];
|
||||
const appName = output?.name || 'App';
|
||||
list.push({
|
||||
id: cardKey,
|
||||
label: output?.name || 'App',
|
||||
label: appName,
|
||||
rect: vc,
|
||||
tileBg: 'linear-gradient(135deg, #ef9552, #d96a2b)',
|
||||
icon: <CoPresentIcon sx={{ fontSize: 16, color: '#fff' }} />,
|
||||
icon: (
|
||||
<Typography sx={{ fontSize: 14, fontWeight: 700, color: '#fff', lineHeight: 1 }}>
|
||||
{appName.charAt(0).toUpperCase()}
|
||||
</Typography>
|
||||
),
|
||||
thumbnail: output?.thumbnail,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
|
||||
@@ -10,6 +10,26 @@ import { useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
*/
|
||||
function DesktopWallpaper(): React.ReactElement {
|
||||
const { mode } = useThemeMode();
|
||||
// The user's real OS wallpaper when the Electron bridge can supply it (never bundled); SVG scenery otherwise.
|
||||
const [realWallpaper, setRealWallpaper] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
const getWallpaper = (window as unknown as { openswarm?: { getDesktopWallpaper?: () => Promise<string | null> } })
|
||||
.openswarm?.getDesktopWallpaper;
|
||||
if (!getWallpaper) return;
|
||||
let cancelled = false;
|
||||
getWallpaper().then((dataUrl) => { if (!cancelled && dataUrl) setRealWallpaper(dataUrl); }).catch(() => undefined);
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
if (realWallpaper) {
|
||||
return (
|
||||
<Box aria-hidden sx={{ position: 'absolute', inset: 0, pointerEvents: 'none', overflow: 'hidden' }}>
|
||||
<Box component="img" src={realWallpaper} alt="" sx={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
{mode === 'dark' && <Box sx={{ position: 'absolute', inset: 0, background: 'rgba(12,6,24,0.38)' }} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
aria-hidden
|
||||
|
||||
Reference in New Issue
Block a user