[eric] workflows: traffic-light window controls + fullscreen (hides dock)

This commit is contained in:
ciregenz
2026-07-21 20:17:50 -07:00
parent 582084996a
commit 669deb3076
5 changed files with 62 additions and 23 deletions
@@ -172,6 +172,8 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
// macOS full screen: one card owns the whole window, every piece of chrome steps aside; Esc exits.
const dispatch = useAppDispatch();
const fullscreenCardId = useAppSelector(selectFullscreenCardId);
// The Workflows window has its own fullscreen flag (not a tiledCard); its fill also hides the dock.
const anyFullscreen = !!fullscreenCardId || !!workflowsHub?.fullscreen;
const [headerRevealed, setHeaderRevealed] = React.useState(false);
const [appsWindowOpen, setAppsWindowOpen] = React.useState(false);
useEffect(() => {
@@ -263,7 +265,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
/>
)}
{!fullscreenCardId && (
{!anyFullscreen && (
<DesktopDock
sessions={sessions}
cards={cards}
@@ -11,6 +11,9 @@ interface WindowControlsProps {
// Full size view: the native macOS lights sit right above this corner, so a second dot cluster
// reads as double chrome; collapse to ONE exit control and let the natives own window ops.
fullscreen?: boolean;
// Green = direct fullscreen toggle, no Fill/Halves/Quarters submenu (for surfaces that only
// support fullscreen, like the Workflows window, where half-tiling has nowhere to land).
noTileMenu?: boolean;
}
// macOS-style traffic lights on every card = the "AI OS" window feel. Grey at rest so a canvas
@@ -54,10 +57,10 @@ export const ARC_CHIP_SX: Record<string, unknown> = {
'.osw-pill-host:hover & .osw-window-lights > :nth-of-type(3)': { transform: 'translate(calc(-50% + 11px), calc(-50% + 5px)) scale(1)', opacity: 1, transitionDelay: '80ms' },
};
function WindowControls({ onClose, onMinimize, onTile, tiled, fullscreen }: WindowControlsProps): React.ReactElement {
function WindowControls({ onClose, onMinimize, onTile, tiled, fullscreen, noTileMenu }: WindowControlsProps): React.ReactElement {
const [menuOpen, setMenuOpen] = useState(false);
const closeTimer = useRef<number | null>(null);
const openMenu = (): void => { if (closeTimer.current) window.clearTimeout(closeTimer.current); setMenuOpen(true); };
const openMenu = (): void => { if (noTileMenu) return; if (closeTimer.current) window.clearTimeout(closeTimer.current); setMenuOpen(true); };
const scheduleClose = (): void => { closeTimer.current = window.setTimeout(() => setMenuOpen(false), 180); };
const stop = (e: React.PointerEvent | React.MouseEvent): void => { e.stopPropagation(); };
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useAppDispatch } from '@/shared/hooks';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { setWorkflowsHubPosition, setWorkflowsHubSize } from '@/shared/state/dashboardLayoutSlice';
import { useTiledStyle } from '@/app/pages/Dashboard/cards/tileZones';
import { useWC } from './uiKit';
import WorkflowsAppContent from './WorkflowsAppContent';
@@ -53,6 +54,18 @@ const WorkflowsAppCard: React.FC<Props> = ({
}) => {
const WC = useWC();
const dispatch = useAppDispatch();
const isFullscreen = useAppSelector((s) => !!s.dashboardLayout.workflowsHub?.fullscreen);
// Fullscreen pins the card to the viewport, so its geometry must track pan/zoom like the tiled
// agent/browser cards; reuse the exact same helper. Subscribe to pan only while fullscreen.
const [, forceTick] = useState(0);
useEffect(() => {
if (!isFullscreen) return undefined;
const onPan = (): void => forceTick((t) => t + 1);
window.addEventListener('openswarm:canvas-pan-changed', onPan);
return () => window.removeEventListener('openswarm:canvas-pan-changed', onPan);
}, [isFullscreen]);
const cam = getCanvasState();
const fsStyle = useTiledStyle(isFullscreen ? 'fullscreen' : undefined, cam.panX, cam.panY, cam.zoom);
// ---- Drag (title bar is the handle) ----
@@ -67,6 +80,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
const onHeaderPointerDown = useCallback((e: React.PointerEvent) => {
if (e.button !== 0) return;
if (isFullscreen) return; // pinned to the viewport, no drag until restored
const target = e.target as HTMLElement;
if (target.closest('[data-no-drag], button, [role="button"], input, textarea, select')) return;
e.preventDefault();
@@ -197,15 +211,20 @@ const WorkflowsAppCard: React.FC<Props> = ({
position: 'absolute',
contain: 'layout style',
willChange: 'transform',
left: dx, top: dy, width: dw, height: dh,
left: fsStyle ? fsStyle.left : dx,
top: fsStyle ? fsStyle.top : dy,
width: fsStyle ? fsStyle.width : dw,
height: fsStyle ? fsStyle.height : dh,
transform: fsStyle ? fsStyle.transform : undefined,
transformOrigin: fsStyle ? fsStyle.transformOrigin : undefined,
background: WC.page,
border,
border: fsStyle ? 'none' : border,
borderRadius: WC.radius.lg,
boxShadow: (isDragging || isResizing) ? WC.shadow.lg : WC.shadow.md,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
zIndex: (isDragging || isResizing) ? 999999 : cardZOrder,
zIndex: fsStyle ? 999990 : (isDragging || isResizing) ? 999999 : cardZOrder,
transition: noTransition ? 'none' : 'box-shadow 0.3s ease, border-color 0.2s ease',
}}
>
@@ -218,7 +237,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
}}
/>
{HANDLE_DEFS.map(({ dir, css }) => (
{!isFullscreen && HANDLE_DEFS.map(({ dir, css }) => (
<div
key={dir}
data-no-drag
@@ -1,14 +1,12 @@
import React, { useEffect, useMemo, useState } from 'react';
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
import CloseIcon from '@mui/icons-material/Close';
import IconButton from '@mui/material/IconButton';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { clearWorkflowsAppTarget, closeWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
import { clearWorkflowsAppTarget, closeWorkflowsApp, toggleWorkflowsHubFullscreen } from '@/shared/state/dashboardLayoutSlice';
import WindowControls from '@/app/pages/Dashboard/cards/WindowControls';
import {
fetchWorkflows, fetchAllRuns, fetchPausedState, fetchActiveRuns, fetchDeletedWorkflows,
} from '@/shared/state/workflowsSlice';
import { fetchMissedRuns } from '@/shared/state/missedRunsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import ShareButton from '@/app/components/share/ShareButton';
import { FONT_SANS, FONT_SERIF, useWC } from './uiKit';
import type { AppMode, CalView, AppNav, CardHeader } from './types';
@@ -22,9 +20,9 @@ import TrashView from './TrashView';
// The three-pane Workflows body plus its title bar. The card wraps this with drag/resize geometry and passes the drag handlers in; the title bar lives here because Share needs to know which workflow is open.
const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => {
const WC = useWC();
const c = useClaudeTokens();
const dispatch = useAppDispatch();
const target = useAppSelector((s) => s.dashboardLayout.workflowsAppTarget);
const isFullscreen = useAppSelector((s) => !!s.dashboardLayout.workflowsHub?.fullscreen);
const dashboardId = useAppSelector((s) => s.tempState.lastDashboardId) || undefined;
const [mode, setMode] = useState<AppMode>('home');
@@ -74,6 +72,23 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => {
onPointerUp={header.onPointerUp}
style={{ height: 42, flex: 'none', display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: `1px solid ${WC.line}`, background: WC.panel, gap: 14, cursor: header.dragging ? 'grabbing' : 'grab', touchAction: 'none', userSelect: 'none' }}
>
{/* macOS traffic lights: the whole window gets close / minimize / full size view like every card. */}
<span
className="osw-card"
data-no-drag
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
style={{ display: 'flex', alignItems: 'center' }}
>
<WindowControls
onClose={() => dispatch(closeWorkflowsApp())}
onMinimize={() => dispatch(closeWorkflowsApp())}
onTile={() => dispatch(toggleWorkflowsHubFullscreen())}
tiled={isFullscreen}
fullscreen={isFullscreen}
noTileMenu
/>
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<EventRepeatIcon sx={{ fontSize: 18, color: WC.accent, display: 'block' }} />
<span style={{ fontFamily: FONT_SERIF, fontSize: 14.5, fontWeight: 500, color: WC.ink, letterSpacing: '-0.01em', lineHeight: 1, transform: 'translateY(2.5px)' }}>Workflows</span>
@@ -93,16 +108,6 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => {
/>
</span>
)}
<IconButton
aria-label="Close"
data-no-drag
size="small"
onClick={(e) => { e.stopPropagation(); dispatch(closeWorkflowsApp()); }}
onPointerDown={(e) => e.stopPropagation()}
sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error, bgcolor: `${c.status.error}14` } }}
>
<CloseIcon fontSize="small" />
</IconButton>
</div>
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
@@ -100,6 +100,8 @@ export interface WorkflowsHubPosition {
width: number;
height: number;
zOrder: number;
// Full size view: the card fills the whole dashboard (reuses the fullscreen tile geometry).
fullscreen?: boolean;
}
@@ -1125,6 +1127,13 @@ const dashboardLayoutSlice = createSlice({
state.workflowsMonitorCard = null;
},
toggleWorkflowsHubFullscreen(state) {
if (state.workflowsHub) {
state.workflowsHub.fullscreen = !state.workflowsHub.fullscreen;
state.workflowsHub.zOrder = state.nextZOrder++;
}
},
clearWorkflowsAppTarget(state) {
state.workflowsAppTarget = null;
},
@@ -1823,6 +1832,7 @@ export const {
closeWorkflowsHub,
openWorkflowsApp,
closeWorkflowsApp,
toggleWorkflowsHubFullscreen,
clearWorkflowsAppTarget,
openWorkflowMonitor,
closeWorkflowMonitor,