[eric] desktop: Spaces strip goes Mission Control scale w/ thumbnails, hover header unclipped, fullscreen keeps all 3 lights, card epithets personalized by prep

This commit is contained in:
ciregenz
2026-07-27 19:40:45 -07:00
parent d42fd08d8d
commit cf37c76331
13 changed files with 91 additions and 75 deletions
+2
View File
@@ -57,6 +57,8 @@ class PrepResponse(BaseModel):
# A punchy <=10-word identity hook, read at a glance in the reveal's focal beat (the greeting is the
# longer warm read for the chat; the headline is the scannable one-liner most people actually read).
headline: str = ""
# 2-4 word identity titles tailored to this user; the Swarm Card leads with these over the static list.
epithets: List[str] = Field(default_factory=list)
greeting: str = ""
starters: List[PersonalizedStarter] = Field(default_factory=list)
app_title: str = ""
+4 -1
View File
@@ -30,7 +30,7 @@ P_SYSTEM = (
"You write first-run starter tasks for OpenSwarm, a desktop AI agent platform that can "
"organize local files, browse the web in a real browser, build small apps, and run agents in parallel. "
"Given facts about the user's machine and the apps they picked, respond with STRICT JSON only: "
'{"headline": string, "greeting": string, "starters": [{"title": string, "prompt": string, "reason": string}], "app_title": string, "app_prompt": string, "app_reason": string, "research_title": string, "research_prompt": string, "research_reason": string, "browser_title": string, "browser_prompt": string, "browser_reason": string, "automations": [{"title": string, "prompt": string, "cadence": "daily"|"weekday"|"weekly"}]}. '
'{"headline": string, "greeting": string, "epithets": [string, string, string], "starters": [{"title": string, "prompt": string, "reason": string}], "app_title": string, "app_prompt": string, "app_reason": string, "research_title": string, "research_prompt": string, "research_reason": string, "browser_title": string, "browser_prompt": string, "browser_reason": string, "automations": [{"title": string, "prompt": string, "cadence": "daily"|"weekday"|"weekly"}]}. '
"First, silently infer a short, confident profile of this user: who they are and what they are working on. "
"If usage_summary is present it is the STRONGEST signal (a distilled profile of who this person is and what "
"they actually work on, read from their real AI conversations); weight it above everything else. Do NOT let a "
@@ -126,6 +126,7 @@ P_SYSTEM = (
"defining trait, no filler, no full sentence, no period. It is read at a glance in big type, so it must NOT be "
"a paragraph. Example shapes only (never copy, tailor to THEM): 'OpenSwarm founder who measures everything, "
"vertical jump to agent latency' or 'Ships iOS apps, obsessed with the last 5% of polish'. Sharp, not wordy. "
"epithets: exactly 3 short identity titles for this person, 2-4 plain words each (like 'QUIET POWER USER' but THEIRS, drawn from their real work and interests in the profile), confident and warm, never punny, never generic. "
"The greeting is one or two warm, punchy sentences that make this person feel INSTANTLY understood, the "
"'wait, it actually gets me' hook. Lead with the single most specific true thing about them from the profile "
"(their actual project BY NAME, their real craft, the obsession they keep returning to), then add ONE more "
@@ -165,6 +166,7 @@ def parse_prep(text: str) -> Optional[PrepResponse]:
starters = build_starters(data.get("starters") if isinstance(data.get("starters"), list) else [])
automations = p_build_automations(data.get("automations") if isinstance(data.get("automations"), list) else [])
headline = str(data.get("headline", "")).strip()
epithets = [strip_dashes(str(x)).strip() for x in (data.get("epithets") or []) if str(x).strip()][:3]
greeting = str(data.get("greeting", "")).strip()
app_title = str(data.get("app_title", "")).strip()
app_prompt = str(data.get("app_prompt", "")).strip()
@@ -213,6 +215,7 @@ def parse_prep(text: str) -> Optional[PrepResponse]:
return None
return PrepResponse(
headline=strip_dashes(headline),
epithets=epithets,
greeting=strip_dashes(greeting),
starters=starters[:4],
app_title=strip_dashes(app_title),
@@ -208,7 +208,7 @@ const OnboardingV3Root: React.FC = () => {
)}
{beat === 'apps' && <BeatApps c={c} picks={picks} setPicks={setPicks} onNext={leaveApps} onBack={() => setBeat('connect')} />}
{beat === 'theme' && <BeatTheme c={c} onNext={() => setBeat('card')} onBack={() => setBeat('apps')} />}
{beat === 'card' && <BeatCard c={c} identity={pipeline.identity} onFinish={(name) => { void leaveCard(name); }} onBack={() => setBeat('theme')} />}
{beat === 'card' && <BeatCard c={c} identity={pipeline.identity} personalizedEpithets={pipeline.getPrepEpithets()} onFinish={(name) => { void leaveCard(name); }} onBack={() => setBeat('theme')} />}
</motion.div>
</AnimatePresence>
</motion.div>
@@ -47,16 +47,24 @@ function readableInk(base: string, c: ClaudeTokens): string {
const BeatCard: React.FC<{
c: ClaudeTokens;
identity: ProviderIdentity[];
// Prep-written identity titles for THIS user; the dice leads with these, statics are the floor.
personalizedEpithets?: string[];
onFinish: (name: string | null) => void;
onBack: () => void;
}> = ({ c, identity, onFinish, onBack }) => {
}> = ({ c, identity, personalizedEpithets, onFinish, onBack }) => {
const { accent, gradient } = useThemeAccent();
const [name, setName] = useState(() => nameFromIdentity(identity));
// finish() can legitimately wait up to PREP_WAIT_CAP_MS on prep; the button must say so, once.
const [submitting, setSubmitting] = useState(false);
const seed = useMemo(() => Math.floor(Math.random() * EPITHETS.length), []);
const pool = useMemo(() => {
const personal = (personalizedEpithets ?? []).map((e) => e.toUpperCase()).filter(Boolean);
return personal.length > 0 ? [...personal, ...EPITHETS] : EPITHETS;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Personalized titles lead; only a pure-static pool starts on a random one.
const seed = useMemo(() => ((personalizedEpithets ?? []).length > 0 ? 0 : Math.floor(Math.random() * pool.length)), [pool]);
const [roll, setRoll] = useState(0);
const epithet = EPITHETS[(seed + roll) % EPITHETS.length];
const epithet = pool[(seed + roll) % pool.length];
const today = useMemo(() => new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }), []);
const [tilt, setTilt] = useState<{ rx: number; ry: number; mx: number; my: number } | null>(null);
const [copied, setCopied] = useState(false);
@@ -25,6 +25,7 @@ export interface ScanResult {
export interface PrepResponse {
headline: string;
epithets?: string[];
greeting: string;
starters: PersonalizedStarter[];
app_title: string;
@@ -118,5 +118,8 @@ export function useOnboardingV3Pipeline() {
dispatch(setFlowActive(false));
}, [dispatch, accent, gradient, mode]);
return { identity, kickIdentity, kickScan, kickUsageRead, kickPrep, finish };
// Read synchronously at card-beat time: prep has usually resolved by the last beat.
const getPrepEpithets = useCallback((): string[] => prepReadyRef.current?.epithets ?? [], []);
return { identity, kickIdentity, kickScan, kickUsageRead, kickPrep, finish, getPrepEpithets };
}
@@ -906,6 +906,8 @@ const AgentCard: React.FC<Props> = ({
border: isFullscreen ? 'none' : isSelected ? '2px solid #3b82f6' : '1px solid rgba(255,255,255,0.08)',
borderRadius: tiledStyle ? '12px' : '20px',
boxShadow: '0 18px 48px rgba(0,0,0,0.4)',
// The hover header floats ABOVE the card; the root must not clip it (the chat body clips itself).
...(tiledStyle ? {} : { overflow: 'visible' }),
}),
}}
>
@@ -1059,7 +1061,7 @@ const AgentCard: React.FC<Props> = ({
onPointerDown={(e) => e.stopPropagation()}
sx={{ display: 'flex', alignItems: 'center', mr: 0.75, flexShrink: 0 }}
>
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} fullscreen={isFullscreen} />
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} />
</Box>
<Box
sx={{
@@ -1002,7 +1002,6 @@ const BrowserCard: React.FC<Props> = ({
onMinimize={handleMinimize}
onTile={onTile}
tiled={!!tileZone}
fullscreen={tileZone === 'fullscreen'}
/>
</Box>
<Box
@@ -636,7 +636,7 @@ const DashboardViewCard: React.FC<Props> = ({
}}
>
<Box onPointerDown={(e) => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center', flexShrink: 0, mr: 0.25 }}>
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} fullscreen={tileZone === 'fullscreen'} />
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} />
</Box>
{!isMinimized && <GridViewRoundedIcon sx={{ fontSize: 16, color: c.accent.primary, flexShrink: 0 }} />}
<Typography
@@ -342,7 +342,7 @@ const NoteCard: React.FC<Props> = ({
}}
>
<Box onPointerDown={(e) => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center' }}>
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} fullscreen={tileZone === 'fullscreen'} />
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} />
</Box>
{isMinimized && (
<Box sx={{ flex: 1, minWidth: 0, fontSize: '0.8125rem', color: palette.text, opacity: 0.75, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
@@ -1,6 +1,5 @@
import React, { useRef, useState } from 'react';
import Box from '@mui/material/Box';
import FullscreenExitRoundedIcon from '@mui/icons-material/FullscreenExitRounded';
import { TILE_ZONES } from './tileZones';
interface WindowControlsProps {
@@ -8,9 +7,6 @@ interface WindowControlsProps {
onMinimize: () => void;
onTile: (zone: string) => void; // a TILE_ZONES key, or 'restore'
tiled?: boolean;
// 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;
@@ -57,7 +53,7 @@ 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, noTileMenu }: WindowControlsProps): React.ReactElement {
function WindowControls({ onClose, onMinimize, onTile, tiled, noTileMenu }: WindowControlsProps): React.ReactElement {
const [menuOpen, setMenuOpen] = useState(false);
// Menu DOM (12 tiles + labels, ~30 nodes) mounts on first green-dot hover, not per card at boot.
const [menuHot, setMenuHot] = useState(false);
@@ -71,32 +67,6 @@ function WindowControls({ onClose, onMinimize, onTile, tiled, fullscreen, noTile
const scheduleClose = (): void => { closeTimer.current = window.setTimeout(() => setMenuOpen(false), 180); };
const stop = (e: React.PointerEvent | React.MouseEvent): void => { e.stopPropagation(); };
if (fullscreen) {
return (
<Box className="osw-window-lights" onPointerDown={stop} sx={{ display: 'flex', alignItems: 'center', flex: 'none' }}>
<Box
component="button"
type="button"
aria-label="Exit full screen"
title="Exit full screen (Esc)"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); onTile('restore'); }}
onPointerDown={stop}
sx={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
width: 24, height: 24, p: 0, border: 'none', borderRadius: '7px',
// Neutral chip at rest so it reads on ANY ground (the fullscreen chat wash is dark, the
// browser chrome is light); hover goes explicit white-on-dark so it can never disappear.
background: 'rgba(128,128,128,0.28)', color: 'inherit', opacity: 0.9, cursor: 'pointer',
transition: 'opacity 120ms, background 120ms, color 120ms',
'&:hover': { opacity: 1, background: 'rgba(0,0,0,0.5)', color: '#fff' },
}}
>
<FullscreenExitRoundedIcon sx={{ fontSize: 16 }} />
</Box>
</Box>
);
}
const btn = (color: string, symbol: string, onClick: () => void, label: string): React.ReactElement => (
<Box component="button" type="button" aria-label={label}
onClick={(e: React.MouseEvent) => { e.stopPropagation(); onClick(); }} onPointerDown={stop} sx={dotSx(color)}>
@@ -1,15 +1,19 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { Plus } from 'lucide-react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { createDashboard } from '@/shared/state/dashboardsSlice';
// macOS Spaces, one for one: rest the cursor on the very top edge and a translucent bar of
// dashboard "spaces" slides down (Mission Control's spaces row), click switches, + adds one.
// This replaces the sidebar as the dashboard switcher; tools live in the dock, search on Cmd+K.
// macOS Spaces, one for one: rest the cursor on the top edge and the spaces bar slides down,
// full-size thumbnail tiles with the name beneath, + at the right end to add a space. It stays
// up while the cursor is anywhere near the bar and only leaves once you move well below it
// (mouseleave flicker is exactly what Mission Control does not do). Replaces the sidebar.
const HOT_ZONE_PX = 3;
const CLOSE_DELAY_MS = 280;
const TILE_W = 176;
const TILE_H = 104;
const DISMISS_BELOW_PX = 72;
const SpacesStrip: React.FC = () => {
const dispatch = useAppDispatch();
@@ -17,7 +21,7 @@ const SpacesStrip: React.FC = () => {
const location = useLocation();
const dashboards = useAppSelector((s) => s.dashboards.items);
const [open, setOpen] = React.useState(false);
const closeTimer = React.useRef<number | null>(null);
const barRef = React.useRef<HTMLDivElement | null>(null);
const activeId = React.useMemo(() => {
const m = location.pathname.match(/\/dashboard\/([^/]+)/);
@@ -29,32 +33,42 @@ const SpacesStrip: React.FC = () => {
[dashboards],
);
const hold = (): void => { if (closeTimer.current) window.clearTimeout(closeTimer.current); };
const reveal = (): void => { hold(); setOpen(true); };
const scheduleClose = (): void => { hold(); closeTimer.current = window.setTimeout(() => setOpen(false), CLOSE_DELAY_MS); };
// Mission Control dismissal: while open, watch the cursor globally and close only once it has
// moved a comfortable distance BELOW the bar; hovering within or near the bar never flickers.
React.useEffect(() => {
if (!open) return undefined;
const onMove = (e: MouseEvent): void => {
const barBottom = barRef.current?.getBoundingClientRect().bottom ?? 0;
if (e.clientY > barBottom + DISMISS_BELOW_PX) setOpen(false);
};
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, [open]);
const addSpace = (): void => {
void dispatch(createDashboard('Untitled Dashboard')).then((result) => {
if (createDashboard.fulfilled.match(result)) navigate(`/dashboard/${(result.payload as { id: string }).id}`);
if (createDashboard.fulfilled.match(result)) {
navigate(`/dashboard/${(result.payload as { id: string }).id}`);
setOpen(false);
}
});
};
return (
<>
<Box onMouseEnter={reveal} sx={{ position: 'fixed', top: 0, left: 0, right: 0, height: HOT_ZONE_PX, zIndex: 99998 }} />
<Box onMouseEnter={() => setOpen(true)} sx={{ position: 'fixed', top: 0, left: 0, right: 0, height: HOT_ZONE_PX, zIndex: 99998 }} />
<Box
onMouseEnter={reveal}
onMouseLeave={scheduleClose}
ref={barRef}
sx={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 99999,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1,
px: 2, py: 1.25,
background: 'rgba(24,22,28,0.42)',
backdropFilter: 'blur(26px) saturate(150%)',
WebkitBackdropFilter: 'blur(26px) saturate(150%)',
borderBottom: '1px solid rgba(255,255,255,0.08)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: 2.25,
px: 3, pt: 2, pb: 1.75,
background: 'rgba(22,20,26,0.45)',
backdropFilter: 'blur(30px) saturate(150%)',
WebkitBackdropFilter: 'blur(30px) saturate(150%)',
borderBottom: '1px solid rgba(255,255,255,0.07)',
transform: open ? 'translateY(0)' : 'translateY(-110%)',
transition: 'transform 220ms cubic-bezier(0.22,1,0.36,1)',
transition: 'transform 260ms cubic-bezier(0.22,1,0.36,1)',
pointerEvents: open ? 'auto' : 'none',
}}
>
@@ -66,16 +80,31 @@ const SpacesStrip: React.FC = () => {
component="button"
onClick={() => { navigate(`/dashboard/${d.id}`); setOpen(false); }}
sx={{
px: 2, py: 0.8, borderRadius: '9px', cursor: 'pointer',
border: active ? '2px solid rgba(255,255,255,0.85)' : '1px solid rgba(255,255,255,0.18)',
background: active ? 'rgba(255,255,255,0.16)' : 'rgba(255,255,255,0.07)',
color: 'rgba(255,255,255,0.92)', fontFamily: 'inherit', fontSize: '0.8125rem', fontWeight: active ? 600 : 500,
whiteSpace: 'nowrap', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis',
transition: 'background 130ms, border-color 130ms',
'&:hover': { background: 'rgba(255,255,255,0.18)' },
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.75,
p: 0, border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: 'inherit',
'&:hover .osw-space-tile': { boxShadow: '0 0 0 3px rgba(255,255,255,0.65), 0 10px 26px rgba(0,0,0,0.4)' },
}}
>
{d.name || 'Untitled'}
<Box
className="osw-space-tile"
sx={{
width: TILE_W, height: TILE_H, borderRadius: '10px', overflow: 'hidden',
background: 'rgba(255,255,255,0.08)',
boxShadow: active
? '0 0 0 3px rgba(255,255,255,0.9), 0 10px 26px rgba(0,0,0,0.4)'
: '0 0 0 1px rgba(255,255,255,0.14), 0 8px 20px rgba(0,0,0,0.35)',
transition: 'box-shadow 140ms',
}}
>
{d.thumbnail ? (
<Box component="img" src={d.thumbnail} alt="" sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
) : (
<Box sx={{ width: '100%', height: '100%', background: 'linear-gradient(135deg, rgba(255,255,255,0.16), rgba(255,255,255,0.05))' }} />
)}
</Box>
<Typography sx={{ color: 'rgba(255,255,255,0.92)', fontSize: '0.8125rem', fontWeight: active ? 600 : 500, maxWidth: TILE_W, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{d.name || 'Untitled'}
</Typography>
</Box>
);
})}
@@ -85,14 +114,14 @@ const SpacesStrip: React.FC = () => {
onClick={addSpace}
sx={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
width: 34, height: 34, borderRadius: '9px', cursor: 'pointer',
border: '1px dashed rgba(255,255,255,0.35)', background: 'transparent',
color: 'rgba(255,255,255,0.85)',
transition: 'background 130ms',
'&:hover': { background: 'rgba(255,255,255,0.14)' },
width: 64, height: TILE_H, borderRadius: '10px', cursor: 'pointer',
border: '1.5px dashed rgba(255,255,255,0.35)', background: 'rgba(255,255,255,0.05)',
color: 'rgba(255,255,255,0.85)', flexShrink: 0,
transition: 'background 140ms, border-color 140ms',
'&:hover': { background: 'rgba(255,255,255,0.14)', borderColor: 'rgba(255,255,255,0.6)' },
}}
>
<Plus size={16} />
<Plus size={22} />
</Box>
</Box>
</>
@@ -85,7 +85,6 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => {
onMinimize={() => dispatch(closeWorkflowsApp())}
onTile={() => dispatch(toggleWorkflowsHubFullscreen())}
tiled={isFullscreen}
fullscreen={isFullscreen}
noTileMenu
/>
</span>