mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-09 19:27:45 +02:00
[eric] desktop: traffic lights everywhere (browser green+tiling, pill hover chips, arc app lights), fullscreen dark+full+dock-switcher, knob drag, dock glyphs, rest composer removed
This commit is contained in:
@@ -1238,6 +1238,11 @@ function createWindow() {
|
||||
},
|
||||
});
|
||||
|
||||
// Arc-style traffic lights: hidden until the renderer's top-edge hover asks for them.
|
||||
if (process.platform === 'darwin') {
|
||||
try { mainWindow.setWindowButtonVisibility(false); } catch (err) { console.warn('[main] setWindowButtonVisibility failed:', err.message); }
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
// Dev only: OPENSWARM_DEV_URL (full override) or OPENSWARM_DEV_PORT lets a second worktree's Electron point at its own webpack-dev-server instead of colliding on the shared :3000. Packaged builds never hit this branch.
|
||||
mainWindow.loadURL(process.env.OPENSWARM_DEV_URL || `http://localhost:${process.env.OPENSWARM_DEV_PORT || 3000}`);
|
||||
@@ -2828,6 +2833,10 @@ ipcMain.handle('get-auth-token', async () => {
|
||||
ipcMain.on('perf:first-agent-response', () => perfMark('first-agent-response'));
|
||||
|
||||
ipcMain.handle('get-app-version', () => app.getVersion());
|
||||
ipcMain.handle('set-window-buttons-visible', (_e, visible) => {
|
||||
if (process.platform !== 'darwin' || !mainWindow || mainWindow.isDestroyed()) return;
|
||||
try { mainWindow.setWindowButtonVisibility(!!visible); } catch (err) { console.warn('[main] setWindowButtonVisibility failed:', err.message); }
|
||||
});
|
||||
// Phase 2 provenance: the renderer's About panel shows the commit this build
|
||||
// was cut from, so a screenshot is enough to identify the exact code shipped.
|
||||
ipcMain.handle('get-build-info', () => getBuildInfo());
|
||||
|
||||
@@ -41,6 +41,8 @@ contextBridge.exposeInMainWorld('openswarm', {
|
||||
getAuthToken: () => ipcRenderer.invoke('get-auth-token'),
|
||||
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
// Arc-style chrome: the mac traffic lights hide at rest; the dashboard's top-edge hover reveals them.
|
||||
setWindowButtonsVisible: (visible) => ipcRenderer.invoke('set-window-buttons-visible', visible),
|
||||
|
||||
// Phase 2 provenance: { sha, shortSha, builtAt, channel } for the About panel.
|
||||
getBuildInfo: () => ipcRenderer.invoke('get-build-info'),
|
||||
|
||||
@@ -628,7 +628,7 @@ const AppShell: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh', bgcolor: c.bg.secondary }}>
|
||||
{sidebarAway && !sidePeek && (
|
||||
{sidebarAway && !sidePeek && !fullscreenCardId && (
|
||||
<Box onMouseEnter={() => { cancelPeekClose(); setSidePeek(true); }} sx={{ position: 'fixed', top: 0, left: 0, bottom: 0, width: 14, zIndex: 2147483000, pointerEvents: 'auto' }} />
|
||||
)}
|
||||
{/* Top bar dropped (Arc/Zen): a zero-height anchor left only to float the agent-activity island at top-center; the island renders nothing when idle. */}
|
||||
|
||||
@@ -40,17 +40,27 @@ export const SquiggleSlider: React.FC<{ value: number; onChange: (v: number) =>
|
||||
};
|
||||
|
||||
export const Knob: React.FC<{ value: number; onChange: (v: number) => void; size?: number }> = ({ value, onChange, size = 34 }) => {
|
||||
const dragging = useRef<{ startY: number; startV: number } | null>(null);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [grabbing, setGrabbing] = React.useState(false);
|
||||
const angle = -135 + value * 270;
|
||||
// Turn like a physical knob: the indicator chases the pointer's angle around the center.
|
||||
const applyAngle = useCallback((clientX: number, clientY: number) => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const deg = Math.atan2(clientX - (r.left + r.width / 2), (r.top + r.height / 2) - clientY) * (180 / Math.PI);
|
||||
onChange(Math.min(1, Math.max(0, (Math.max(-135, Math.min(135, deg)) + 135) / 270)));
|
||||
}, [onChange]);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
title="Intensity"
|
||||
onPointerDown={(e) => { dragging.current = { startY: e.clientY, startV: value }; (e.target as HTMLElement).setPointerCapture?.(e.pointerId); }}
|
||||
onPointerMove={(e) => { const d = dragging.current; if (!d) return; onChange(Math.min(1, Math.max(0, d.startV + (d.startY - e.clientY) / 120))); }}
|
||||
onPointerUp={() => { dragging.current = null; }}
|
||||
onPointerDown={(e) => { setGrabbing(true); (e.target as HTMLElement).setPointerCapture?.(e.pointerId); applyAngle(e.clientX, e.clientY); }}
|
||||
onPointerMove={(e) => { if (grabbing) applyAngle(e.clientX, e.clientY); }}
|
||||
onPointerUp={() => setGrabbing(false)}
|
||||
style={{
|
||||
position: 'relative', width: size + 10, height: size + 10, display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'center', cursor: 'ns-resize', touchAction: 'none', flexShrink: 0,
|
||||
justifyContent: 'center', cursor: grabbing ? 'grabbing' : 'grab', touchAction: 'none', flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'absolute', inset: 0, borderRadius: 999, border: '2px dotted currentColor', opacity: 0.4 }} />
|
||||
|
||||
@@ -2375,7 +2375,6 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
autoFocus={autoFocus}
|
||||
prefillPrompt={prefillPrompt}
|
||||
placeholderOverride={runContext ? 'Ask about this run...' : embedded ? 'Send a message...' : undefined}
|
||||
quietComposer={embedded}
|
||||
runContext={runContext}
|
||||
onClearRunContext={onClearRunContext}
|
||||
thinkingLevel={session?.thinking_level ?? 'auto'}
|
||||
|
||||
@@ -48,14 +48,12 @@ interface Props {
|
||||
prefillPrompt?: string;
|
||||
// Replaces the default "Agent, @ for context..." placeholder (e.g. "Ask about this run...").
|
||||
placeholderOverride?: string;
|
||||
// Desktop-card composer: rest as input + attach/mic; pickers return on focus.
|
||||
quietComposer?: boolean;
|
||||
// A workflow run shown as a small removable chip inside the composer.
|
||||
runContext?: WorkflowsRunContext;
|
||||
onClearRunContext?: () => void;
|
||||
}
|
||||
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange, prefillPrompt, placeholderOverride, quietComposer, runContext, onClearRunContext }, ref) => {
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange, prefillPrompt, placeholderOverride, runContext, onClearRunContext }, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -322,7 +320,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
editorRef={editorRef}
|
||||
generalFileInputRef={generalFileInputRef}
|
||||
embedded={embedded}
|
||||
quietComposer={quietComposer}
|
||||
isDragOver={isDragOver}
|
||||
isUploading={isUploading}
|
||||
handleDragOver={handleDragOver}
|
||||
|
||||
@@ -50,8 +50,6 @@ interface Props {
|
||||
isRunning?: boolean;
|
||||
onStop?: () => void;
|
||||
handleSend: () => void;
|
||||
/** Embedded-card resting look: only attach + mic; pickers come back on focus. */
|
||||
restMode?: boolean;
|
||||
}
|
||||
|
||||
export const ChatInputToolbar: React.FC<Props> = (p) => {
|
||||
@@ -60,7 +58,7 @@ export const ChatInputToolbar: React.FC<Props> = (p) => {
|
||||
allModelFlat, model, onModelChange, onProviderChange, picker, pendingKinds, pendingPayloadEstimate,
|
||||
thinkingLevel, onThinkingLevelChange, contextEstimate, elementSelection, autoRunMode,
|
||||
ownerId, sessionId, generalFileInputRef, addImageFiles, uploadAndAttachFiles,
|
||||
hasContent, disabled, isRunning, onStop, handleSend, restMode,
|
||||
hasContent, disabled, isRunning, onStop, handleSend,
|
||||
} = p;
|
||||
|
||||
const menuPaperProps = {
|
||||
@@ -96,14 +94,12 @@ export const ChatInputToolbar: React.FC<Props> = (p) => {
|
||||
pt: 0,
|
||||
}}
|
||||
>
|
||||
{!restMode && (
|
||||
<ModelControl
|
||||
c={c}
|
||||
setModelAnchor={setModelAnchor}
|
||||
allModelFlat={allModelFlat}
|
||||
model={model}
|
||||
/>
|
||||
)}
|
||||
<ModelControl
|
||||
c={c}
|
||||
setModelAnchor={setModelAnchor}
|
||||
allModelFlat={allModelFlat}
|
||||
model={model}
|
||||
/>
|
||||
|
||||
<ModelPickerMenu
|
||||
c={c}
|
||||
@@ -138,7 +134,7 @@ export const ChatInputToolbar: React.FC<Props> = (p) => {
|
||||
pendingPayloadEstimate={pendingPayloadEstimate}
|
||||
/>
|
||||
|
||||
{!hideForTrial && !restMode && (
|
||||
{!hideForTrial && (
|
||||
<ThinkingLevelControl
|
||||
c={c}
|
||||
model={model}
|
||||
@@ -153,7 +149,7 @@ export const ChatInputToolbar: React.FC<Props> = (p) => {
|
||||
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
{contextEstimate && !restMode && (
|
||||
{contextEstimate && (
|
||||
<ContextRing
|
||||
used={contextEstimate.used}
|
||||
limit={contextEstimate.limit}
|
||||
@@ -164,7 +160,6 @@ export const ChatInputToolbar: React.FC<Props> = (p) => {
|
||||
|
||||
<ToolbarActions
|
||||
c={c}
|
||||
restMode={restMode}
|
||||
elementSelection={elementSelection}
|
||||
autoRunMode={autoRunMode}
|
||||
ownerId={ownerId}
|
||||
|
||||
@@ -24,16 +24,15 @@ interface Props {
|
||||
isRunning?: boolean;
|
||||
onStop?: () => void;
|
||||
handleSend: () => void;
|
||||
restMode?: boolean;
|
||||
}
|
||||
|
||||
export const ToolbarActions: React.FC<Props> = ({
|
||||
c, elementSelection, autoRunMode, ownerId, sessionId, generalFileInputRef,
|
||||
addImageFiles, uploadAndAttachFiles, hasContent, disabled, isRunning, onStop, handleSend, restMode,
|
||||
addImageFiles, uploadAndAttachFiles, hasContent, disabled, isRunning, onStop, handleSend,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{elementSelection && !autoRunMode && !restMode && (() => {
|
||||
{elementSelection && !autoRunMode && (() => {
|
||||
const isMySelectMode = elementSelection.selectMode && elementSelection.activeOwnerId === ownerId;
|
||||
return (
|
||||
<Tooltip title={isMySelectMode ? 'Exit select mode' : 'Select UI element'}>
|
||||
|
||||
@@ -28,7 +28,6 @@ interface Props {
|
||||
editorRef: RefObject<HTMLDivElement>;
|
||||
generalFileInputRef: RefObject<HTMLInputElement>;
|
||||
embedded?: boolean;
|
||||
quietComposer?: boolean;
|
||||
isDragOver: boolean;
|
||||
isUploading: boolean;
|
||||
handleDragOver: (e: React.DragEvent) => void;
|
||||
@@ -107,18 +106,9 @@ interface Props {
|
||||
|
||||
export const ChatInputView: React.FC<Props> = (p) => {
|
||||
const { c } = p;
|
||||
// Embedded card composers rest as just the input + attach/mic (the frame look); the pickers return on focus, draft text, or any open menu.
|
||||
const [focusWithin, setFocusWithin] = useState(false);
|
||||
const restMode = Boolean(
|
||||
p.quietComposer && !focusWithin && !p.hasContent && !p.modelAnchor && !p.thinkingAnchor && !p.modeAnchor,
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
ref={p.containerRef}
|
||||
onFocusCapture={() => setFocusWithin(true)}
|
||||
onBlurCapture={(e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocusWithin(false);
|
||||
}}
|
||||
onDragOver={p.handleDragOver}
|
||||
onDragLeave={p.handleDragLeave}
|
||||
onDrop={p.handleDrop}
|
||||
@@ -256,7 +246,6 @@ export const ChatInputView: React.FC<Props> = (p) => {
|
||||
|
||||
<ChatInputToolbar
|
||||
c={c}
|
||||
restMode={restMode}
|
||||
modeConf={p.modeConf}
|
||||
modesArr={p.modesArr}
|
||||
mode={p.mode}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, type RefObject } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearTiledCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { clearTiledCard, setTiledCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { expandSession } from '@/shared/state/agentsSlice';
|
||||
import DashboardHeader from './DashboardHeader';
|
||||
import TetherLayer from './TetherLayer';
|
||||
import DashboardCardLayer from './DashboardCardLayer';
|
||||
@@ -185,6 +186,40 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
return () => window.removeEventListener('keydown', onKey, true);
|
||||
}, [fullscreenCardId, dispatch]);
|
||||
|
||||
// Arc-style chrome: the mac traffic lights ride the same top-edge hover as the header overlay.
|
||||
useEffect(() => {
|
||||
window.openswarm?.setWindowButtonsVisible?.(headerRevealed && !fullscreenCardId);
|
||||
}, [headerRevealed, fullscreenCardId]);
|
||||
|
||||
// Reveal on any pointer graze of the top edge. The old 22px strip Box was dead in practice: the
|
||||
// hidden header overlay's pointer-events:auto children sat above it and ate the mouseenter.
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent): void => {
|
||||
if (e.clientY <= 22) setHeaderRevealed(true);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, []);
|
||||
|
||||
// Arc/Zen fullscreen: the dock hides with the rest of the chrome but slides back on a left-edge
|
||||
// graze, and clicking a tile SWAPS which card owns the full screen instead of moving the camera.
|
||||
const [fsDockRevealed, setFsDockRevealed] = React.useState(false);
|
||||
useEffect(() => {
|
||||
if (!fullscreenCardId) { setFsDockRevealed(false); return undefined; }
|
||||
const onMove = (e: MouseEvent): void => {
|
||||
if (e.clientX <= 16) setFsDockRevealed(true);
|
||||
else if (e.clientX > 120) setFsDockRevealed(false);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, [fullscreenCardId]);
|
||||
const swapFullscreen = React.useCallback((cardId: string) => {
|
||||
if (!fullscreenCardId || cardId === fullscreenCardId) return;
|
||||
dispatch(clearTiledCard(fullscreenCardId));
|
||||
if (sessions[cardId]) dispatch(expandSession(cardId));
|
||||
dispatch(setTiledCard({ cardId, zone: 'fullscreen' }));
|
||||
}, [fullscreenCardId, dispatch, sessions]);
|
||||
|
||||
// Gestures write the transform imperatively (no React commit per frame), so a foreign render mid-gesture would paint the stale committed transform for a frame. Re-applying live after EVERY render seals that; do not remove.
|
||||
React.useLayoutEffect(() => {
|
||||
canvas.actions.syncTransform();
|
||||
@@ -193,11 +228,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ position: 'relative', height: '100%', overflow: 'hidden' }}>
|
||||
{/* Top-edge hover strip: the desktop shell keeps the top chromeless; grazing it reveals the header. */}
|
||||
<Box
|
||||
onMouseEnter={() => setHeaderRevealed(true)}
|
||||
sx={{ position: 'absolute', top: 0, left: 0, right: 0, height: 22, zIndex: 9 }}
|
||||
/>
|
||||
{/* Floating header overlay */}
|
||||
<Box
|
||||
onMouseLeave={() => setHeaderRevealed(false)}
|
||||
@@ -251,25 +281,35 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!fullscreenCardId && (
|
||||
<DesktopDock
|
||||
sessions={sessions}
|
||||
cards={cards}
|
||||
viewCards={viewCards}
|
||||
browserCards={browserCards}
|
||||
notes={notes}
|
||||
workflowCards={workflowCards}
|
||||
outputs={outputs}
|
||||
selectedIds={Array.from(selection.selectedIds.keys())}
|
||||
onFocusCard={(cardId, rect) => {
|
||||
canvas.actions.fitToCards([rect], 1.15, true);
|
||||
onHighlightCard?.(cardId);
|
||||
}}
|
||||
onApplications={() => setAppsWindowOpen((v) => !v)}
|
||||
onNewAgent={onNewAgent}
|
||||
onAddBrowser={onAddBrowser}
|
||||
onAddNote={onAddNote}
|
||||
/>
|
||||
{(!fullscreenCardId || fsDockRevealed) && (
|
||||
<Box
|
||||
sx={fullscreenCardId ? {
|
||||
position: 'absolute', left: 0, top: 0, bottom: 0, zIndex: 999995,
|
||||
display: 'flex', alignItems: 'center', pl: '4px',
|
||||
animation: 'osw-fsdock-in 160ms ease-out',
|
||||
'@keyframes osw-fsdock-in': { from: { transform: 'translateX(-52px)', opacity: 0 }, to: { transform: 'translateX(0)', opacity: 1 } },
|
||||
} : undefined}
|
||||
>
|
||||
<DesktopDock
|
||||
sessions={sessions}
|
||||
cards={cards}
|
||||
viewCards={viewCards}
|
||||
browserCards={browserCards}
|
||||
notes={notes}
|
||||
workflowCards={workflowCards}
|
||||
outputs={outputs}
|
||||
selectedIds={Array.from(selection.selectedIds.keys())}
|
||||
onFocusCard={(cardId, rect) => {
|
||||
if (fullscreenCardId) { swapFullscreen(cardId); return; }
|
||||
canvas.actions.fitToCards([rect], 1.15, true);
|
||||
onHighlightCard?.(cardId);
|
||||
}}
|
||||
onApplications={() => setAppsWindowOpen((v) => !v)}
|
||||
onNewAgent={onNewAgent}
|
||||
onAddBrowser={onAddBrowser}
|
||||
onAddNote={onAddNote}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{appsWindowOpen && !fullscreenCardId && (
|
||||
|
||||
@@ -65,7 +65,7 @@ const KEYWORDS: Record<string, LucideIcon> = {
|
||||
weather: CloudSun, forecast: CloudSun, temperature: CloudSun,
|
||||
};
|
||||
|
||||
function pickIcon(title: string): LucideIcon | null {
|
||||
export function pickIcon(title: string): LucideIcon | null {
|
||||
const words = title.toLowerCase().match(/[a-z]+/g) || [];
|
||||
for (const w of words) {
|
||||
const hit = KEYWORDS[w] || (w.endsWith('s') ? KEYWORDS[w.slice(0, -1)] : undefined);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
AgentSession,
|
||||
handleApproval,
|
||||
collapseSession,
|
||||
expandSession,
|
||||
closeSession,
|
||||
fetchSession,
|
||||
renameSession,
|
||||
@@ -670,10 +671,18 @@ const AgentCard: React.FC<Props> = ({
|
||||
void tileTick;
|
||||
const cam = getCanvasState();
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom);
|
||||
// A collapsed chat can never stay tiled: collapsing while fullscreen left a white full-window shell
|
||||
// (the header collapse control still fires in full size view). Seal the state instead of the path.
|
||||
useEffect(() => {
|
||||
if (tileZone && !expanded) dispatch(clearTiledCard(session.id));
|
||||
}, [tileZone, expanded, dispatch, session.id]);
|
||||
const onMinimize = (): void => { dispatch(collapseSession(session.id)); };
|
||||
const onTile = (zone: string): void => {
|
||||
if (zone === 'restore') dispatch(clearTiledCard(session.id));
|
||||
else dispatch(setTiledCard({ cardId: session.id, zone }));
|
||||
else {
|
||||
if (!expanded) dispatch(expandSession(session.id));
|
||||
dispatch(setTiledCard({ cardId: session.id, zone }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -913,13 +922,18 @@ const AgentCard: React.FC<Props> = ({
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
'&:hover': {},
|
||||
}),
|
||||
// Expanded chat wears the desktop dark glass; the header only surfaces on hover.
|
||||
...(expanded && !tiledStyle && {
|
||||
bgcolor: 'rgba(26,16,34,0.85)',
|
||||
backdropFilter: 'blur(24px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(150%)',
|
||||
// Expanded chat wears the desktop dark glass; the header only surfaces on hover. Tiled keeps
|
||||
// the SAME dark surface (excluding it rendered the light-theme white card, the "fullscreen
|
||||
// turns white" bug) but solid + blur-free: nothing shows behind a tiled card, and a
|
||||
// window-sized backdrop blur is pure GPU tax.
|
||||
...(expanded && {
|
||||
bgcolor: tiledStyle ? 'rgb(26,16,34)' : 'rgba(26,16,34,0.85)',
|
||||
...(tiledStyle ? {} : {
|
||||
backdropFilter: 'blur(24px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(150%)',
|
||||
}),
|
||||
border: isSelected ? '2px solid #3b82f6' : '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: '20px',
|
||||
borderRadius: tiledStyle ? '12px' : '20px',
|
||||
boxShadow: '0 18px 48px rgba(0,0,0,0.4)',
|
||||
}),
|
||||
}}
|
||||
@@ -968,8 +982,25 @@ const AgentCard: React.FC<Props> = ({
|
||||
onPointerDown={handleDragPointerDown}
|
||||
onPointerMove={handleDragPointerMove}
|
||||
onPointerUp={handleDragPointerUp}
|
||||
sx={{ touchAction: 'none', userSelect: 'none' }}
|
||||
sx={{ position: 'relative', touchAction: 'none', userSelect: 'none', pt: '26px', mt: '-26px', '&:hover .osw-pill-lights': { opacity: 1, pointerEvents: 'auto' } }}
|
||||
>
|
||||
<Box
|
||||
className="osw-pill-lights osw-card"
|
||||
onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
|
||||
sx={{
|
||||
position: 'absolute', top: 0, left: 4, zIndex: 2, display: 'flex', alignItems: 'center',
|
||||
px: 1, py: 0.5, borderRadius: 999, background: 'rgba(24,14,32,0.85)',
|
||||
backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)',
|
||||
opacity: 0, pointerEvents: 'none', transition: 'opacity 140ms ease',
|
||||
}}
|
||||
>
|
||||
<WindowControls
|
||||
onClose={() => handleRemove()}
|
||||
onMinimize={() => dispatch(expandSession(session.id))}
|
||||
onTile={(zone: string) => { dispatch(expandSession(session.id)); onTile(zone); }}
|
||||
tiled={false}
|
||||
/>
|
||||
</Box>
|
||||
<AgentNarratorPill
|
||||
label={pillLabel}
|
||||
running={pillRunning}
|
||||
|
||||
@@ -38,8 +38,12 @@ import {
|
||||
moveBrowserTab,
|
||||
recordClosedCard,
|
||||
toggleMinimizeCard,
|
||||
setTiledCard,
|
||||
clearTiledCard,
|
||||
type BrowserTab,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import WindowControls from './WindowControls';
|
||||
import { useTiledStyle } from './tileZones';
|
||||
import { saveMinimizedShot } from '../desktop/minimizedShots';
|
||||
import { removeBrowserCardCleanly } from '@/shared/browserTeardown';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
@@ -71,17 +75,6 @@ const CHROME_BORDER = 'rgba(0,0,0,0.08)';
|
||||
const CHROME_TEXT = '#3c3744';
|
||||
const CHROME_TEXT_MUTED = '#8a8494';
|
||||
|
||||
const browserLightSx = (color: string): Record<string, unknown> => ({
|
||||
width: 12,
|
||||
height: 12,
|
||||
p: 0,
|
||||
borderRadius: '50%',
|
||||
border: '0.5px solid rgba(0,0,0,0.08)',
|
||||
background: '#d6d3cd',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 150ms',
|
||||
'.osw-card:hover &': { background: color },
|
||||
});
|
||||
import { useElementSelection } from '@/app/components/editor/ElementSelectionContext';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
@@ -229,6 +222,22 @@ const BrowserCard: React.FC<Props> = ({
|
||||
);
|
||||
const browserAgentSession = useAppSelector(selectBrowserAgentSession);
|
||||
const isMinimized = useAppSelector((s) => Boolean(s.dashboardLayout.minimizedCards[browserId]));
|
||||
const tileZone = useAppSelector((s) => s.dashboardLayout.tiledCards[browserId]);
|
||||
// Tiled geometry must track pan/zoom, but the camera lives outside React now; subscribe to the pan event ONLY while tiled and read the live getter.
|
||||
const [tileTick, setTileTick] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!tileZone) return undefined;
|
||||
const onPan = (): void => setTileTick((t) => t + 1);
|
||||
window.addEventListener('openswarm:canvas-pan-changed', onPan);
|
||||
return () => window.removeEventListener('openswarm:canvas-pan-changed', onPan);
|
||||
}, [tileZone]);
|
||||
void tileTick;
|
||||
const cam = getCanvasState();
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom);
|
||||
const onTile = useCallback((zone: string): void => {
|
||||
if (zone === 'restore') dispatch(clearTiledCard(browserId));
|
||||
else dispatch(setTiledCard({ cardId: browserId, zone }));
|
||||
}, [dispatch, browserId]);
|
||||
|
||||
const suspendedSnap = useAppSelector((state) => state.dashboardLayout.suspendedBrowserCards[browserId]);
|
||||
const endingState = useAppSelector((state) => state.dashboardLayout.endingBrowserCards[browserId]);
|
||||
@@ -546,8 +555,11 @@ const BrowserCard: React.FC<Props> = ({
|
||||
const handleMinimize = useCallback(() => {
|
||||
const wv = webviewMap.current.get(activeTabId);
|
||||
const capture = wv?.capturePage?.();
|
||||
const park = (): void => { dispatch(toggleMinimizeCard({ cardId: browserId })); };
|
||||
let parked = false;
|
||||
const park = (): void => { if (parked) return; parked = true; dispatch(toggleMinimizeCard({ cardId: browserId })); };
|
||||
if (capture && typeof (capture as Promise<unknown>).then === 'function') {
|
||||
// capturePage can hang forever on off-screen guests (Electron 42); the timer guarantees the park.
|
||||
window.setTimeout(park, 800);
|
||||
(capture as Promise<{ toDataURL(): string }>)
|
||||
.then((img) => { saveMinimizedShot(browserId, img.toDataURL()); })
|
||||
.catch(() => undefined)
|
||||
@@ -917,19 +929,20 @@ const BrowserCard: React.FC<Props> = ({
|
||||
contain: 'layout style',
|
||||
// Own compositor layer so hover/paint invalidations stay contained to this card. See AgentCard for full rationale.
|
||||
willChange: 'transform',
|
||||
left: keepAliveHidden || isMinimized ? -100000 : (dragging ? cardX : displayX),
|
||||
top: dragging ? cardY : displayY,
|
||||
transform: dragging ? `translate3d(${dragTx}px, ${dragTy}px, 0)` : undefined,
|
||||
width: displayW,
|
||||
height: displayH,
|
||||
borderRadius: `${c.radius.lg}px`,
|
||||
left: keepAliveHidden || isMinimized ? -100000 : (tiledStyle ? tiledStyle.left : (dragging ? cardX : displayX)),
|
||||
top: tiledStyle && !(keepAliveHidden || isMinimized) ? tiledStyle.top : (dragging ? cardY : displayY),
|
||||
transform: tiledStyle ? tiledStyle.transform : (dragging ? `translate3d(${dragTx}px, ${dragTy}px, 0)` : undefined),
|
||||
transformOrigin: tiledStyle ? tiledStyle.transformOrigin : undefined,
|
||||
width: tiledStyle ? tiledStyle.width : displayW,
|
||||
height: tiledStyle ? tiledStyle.height : displayH,
|
||||
borderRadius: tileZone === 'fullscreen' ? '12px' : `${c.radius.lg}px`,
|
||||
border: agentBorder,
|
||||
bgcolor: c.bg.surface,
|
||||
boxShadow: agentShadow,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
zIndex: (isDragging || isResizing) ? 999999 : cardZOrder,
|
||||
zIndex: tiledStyle ? 999990 : (isDragging || isResizing) ? 999999 : cardZOrder,
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease',
|
||||
'&:hover .resize-handle': { opacity: 1 },
|
||||
...(isHighlighted && {
|
||||
@@ -979,21 +992,13 @@ const BrowserCard: React.FC<Props> = ({
|
||||
>
|
||||
<Box
|
||||
onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: '7px', pl: 1.25, pr: 0.75, flexShrink: 0 }}
|
||||
sx={{ display: 'flex', alignItems: 'center', pl: 1.25, pr: 0.75, flexShrink: 0 }}
|
||||
>
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
aria-label="Close browser"
|
||||
onClick={handleRemove}
|
||||
sx={{ ...browserLightSx('#ff5f57'), }}
|
||||
/>
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
aria-label="Minimize browser"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleMinimize(); }}
|
||||
sx={{ ...browserLightSx('#febc2e'), }}
|
||||
<WindowControls
|
||||
onClose={() => { dispatch(recordClosedCard({ kind: 'browser', id: browserId })); removeBrowserCardCleanly(browserId, dispatch); }}
|
||||
onMinimize={handleMinimize}
|
||||
onTile={onTile}
|
||||
tiled={!!tileZone}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
|
||||
@@ -47,11 +47,10 @@ function workspaceSize(): { w: number; h: number } {
|
||||
|
||||
export function computeTiledStyle(zone: string, panX: number, panY: number, zoom: number): TiledStyle | null {
|
||||
// 'fullscreen' = macOS full screen: the app chrome hides (AppShell/DashboardCanvas react to
|
||||
// selectFullscreenCardId) and the card covers the window minus a thin PEEK sliver, the Zen/Arc
|
||||
// touch where the surroundings stay ever-so-slightly visible. Target is WINDOW space here, so
|
||||
// selectFullscreenCardId) and the card covers the WHOLE window. Target is WINDOW space here, so
|
||||
// the viewport origin does NOT cancel; once the chrome collapses that origin goes to ~0 anyway.
|
||||
if (zone === 'fullscreen') {
|
||||
const PEEK = 10;
|
||||
const PEEK = 0;
|
||||
const el = document.querySelector('[data-canvas-viewport]');
|
||||
const r = el ? el.getBoundingClientRect() : null;
|
||||
const ox = r ? r.left : 0;
|
||||
|
||||
@@ -7,7 +7,8 @@ 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 { pickIcon } from '../canvas/DashboardGlyph';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { openWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import AppsRoundedIcon from '@mui/icons-material/AppsRounded';
|
||||
@@ -107,12 +108,13 @@ function DesktopDock({
|
||||
const session = sessions[card.session_id];
|
||||
if (!session) continue;
|
||||
const title = displayChatTitle(session);
|
||||
const ChatIcon = pickIcon(title) || MessageSquare;
|
||||
list.push({
|
||||
id: card.session_id,
|
||||
label: title,
|
||||
rect: card,
|
||||
tileBg: hueFor(title),
|
||||
icon: <DashboardGlyph name={title} size={16} color="#fff" />,
|
||||
icon: <ChatIcon size={16} strokeWidth={1.75} color="#fff" />,
|
||||
snippet: session.turn_label?.label || undefined,
|
||||
});
|
||||
}
|
||||
@@ -230,6 +232,7 @@ function DesktopDock({
|
||||
onFocusCard(entry.id, entry.rect);
|
||||
}}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
borderRadius: '9px',
|
||||
@@ -245,15 +248,15 @@ function DesktopDock({
|
||||
...(isActive && { outline: '2px solid #6aa2ff', outlineOffset: '2px' }),
|
||||
}}
|
||||
>
|
||||
{entry.faviconUrl ? (
|
||||
{entry.icon}
|
||||
{entry.faviconUrl && (
|
||||
<Box
|
||||
component="img"
|
||||
src={entry.faviconUrl}
|
||||
alt=""
|
||||
sx={{ width: 18, height: 18, borderRadius: '4px' }}
|
||||
onError={(e: React.SyntheticEvent<HTMLImageElement>) => { e.currentTarget.style.display = 'none'; }}
|
||||
sx={{ position: 'absolute', width: 18, height: 18, borderRadius: '4px' }}
|
||||
/>
|
||||
) : (
|
||||
entry.icon
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,9 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { toggleMinimizeCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { toggleMinimizeCard, setTiledCard, recordClosedCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { removeBrowserCardCleanly } from '@/shared/browserTeardown';
|
||||
import WindowControls from '../cards/WindowControls';
|
||||
import { getMinimizedShot, dropMinimizedShot } from './minimizedShots';
|
||||
import type { BrowserCardPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
|
||||
@@ -44,28 +46,48 @@ function MinimizedStack({ browserCards, onRestore }: MinimizedStackProps): React
|
||||
{entries.map((bc) => {
|
||||
const activeTab = bc.tabs.find((t) => t.id === bc.activeTabId) || bc.tabs[0];
|
||||
const shot = getMinimizedShot(bc.browser_id);
|
||||
const restore = (): void => {
|
||||
dropMinimizedShot(bc.browser_id);
|
||||
dispatch(toggleMinimizeCard({ cardId: bc.browser_id }));
|
||||
onRestore(bc.browser_id, bc);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={bc.browser_id}
|
||||
onClick={() => {
|
||||
dropMinimizedShot(bc.browser_id);
|
||||
dispatch(toggleMinimizeCard({ cardId: bc.browser_id }));
|
||||
onRestore(bc.browser_id, bc);
|
||||
}}
|
||||
onClick={restore}
|
||||
title={activeTab?.title || 'Browser'}
|
||||
className="osw-card"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: THUMB_W,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
|
||||
background: '#fff',
|
||||
transition: 'transform 0.15s ease, box-shadow 0.15s ease',
|
||||
'&:hover': { transform: 'scale(1.06)', boxShadow: '0 10px 28px rgba(0,0,0,0.4)' },
|
||||
'&:hover .osw-pill-lights': { opacity: 1, pointerEvents: 'auto' },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="osw-pill-lights"
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
sx={{
|
||||
position: 'absolute', top: 3, left: 3, zIndex: 2, display: 'flex', alignItems: 'center',
|
||||
px: 1, py: 0.5, borderRadius: 999, background: 'rgba(24,14,32,0.85)',
|
||||
backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)',
|
||||
opacity: 0, pointerEvents: 'none', transition: 'opacity 140ms ease',
|
||||
}}
|
||||
>
|
||||
<WindowControls
|
||||
onClose={() => { dispatch(recordClosedCard({ kind: 'browser', id: bc.browser_id })); removeBrowserCardCleanly(bc.browser_id, dispatch); }}
|
||||
onMinimize={restore}
|
||||
onTile={(zone: string) => { restore(); if (zone !== 'restore') dispatch(setTiledCard({ cardId: bc.browser_id, zone })); }}
|
||||
tiled={false}
|
||||
/>
|
||||
</Box>
|
||||
{shot ? (
|
||||
<Box component="img" src={shot} alt="" sx={{ width: '100%', display: 'block' }} />
|
||||
<Box component="img" src={shot} alt="" sx={{ width: '100%', display: 'block', borderRadius: '8px' }} />
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5, py: 1.5, px: 1 }}>
|
||||
{activeTab?.favicon ? (
|
||||
|
||||
Vendored
+1
@@ -35,6 +35,7 @@ declare global {
|
||||
getBackendPort: () => number;
|
||||
getWebviewPreloadPath: () => string;
|
||||
getAppVersion: () => Promise<string>;
|
||||
setWindowButtonsVisible?: (visible: boolean) => Promise<void>;
|
||||
getBuildInfo: () => Promise<{ sha: string; shortSha: string; builtAt: string | null; channel: string }>;
|
||||
getUpdateStatus: () => Promise<{ status: string; info: any; error: string | null }>;
|
||||
getCrashRecoveryInfo?: () => Promise<{ ts: number; parent_pid: number; uptime_ms: number } | null>;
|
||||
|
||||
Reference in New Issue
Block a user