mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-23 13:02:23 +02:00
[eric] desktop: full size view reads like a chat page (centered reading column), drop the dock rail + fisheye
This commit is contained in:
@@ -84,6 +84,10 @@ const CONTEXT_WINDOWS: Record<string, number> = {
|
||||
haiku: 200_000,
|
||||
};
|
||||
|
||||
// Full size view reads like a chat page, not a card: the transcript + composer center in one
|
||||
// column at a comfortable measure (assistant-ui parks at 44rem, LibreChat 48rem; 760 splits them).
|
||||
const FULLSCREEN_READING_MAX_W = 760;
|
||||
|
||||
// Only a fallback for never-rendered items; real heights are measured once on screen.
|
||||
const RENDER_ITEM_ESTIMATED_HEIGHT = 140;
|
||||
// Conservative estimate for an unmeasured tool row: tool groups/pairs render collapsed (~40-50px) far more often than expanded. Leaning low keeps scrollHeight (and the scrollbar thumb) from jumping when a tool row measures shorter.
|
||||
@@ -228,6 +232,8 @@ interface AgentChatProps {
|
||||
workflowEditId?: string;
|
||||
// View-only transcript (e.g. the Run Monitor): renders messages + tool calls but no composer, so the session can't be typed into.
|
||||
readOnly?: boolean;
|
||||
// Full size view: center the whole chat in a reading column so an expanded card reads like a chat page.
|
||||
fullscreenChat?: boolean;
|
||||
// One-shot text to drop into the composer (e.g. a run attached as context).
|
||||
prefillPrompt?: string;
|
||||
// A workflow run attached as a removable context chip above the composer; while present, each send routes through onSendRunQuestion so the run's transcript rides along as hidden context for that turn.
|
||||
@@ -236,7 +242,7 @@ interface AgentChatProps {
|
||||
onSendRunQuestion?: (prompt: string, runId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose, embedded, autoFocus, isGlowing, onDismissGlow, initialContextPaths, onBranch, workflowEditId, readOnly, prefillPrompt, runContext, onClearRunContext, onSendRunQuestion }) => {
|
||||
const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose, embedded, autoFocus, isGlowing, onDismissGlow, initialContextPaths, onBranch, workflowEditId, readOnly, fullscreenChat, prefillPrompt, runContext, onClearRunContext, onSendRunQuestion }) => {
|
||||
const c = useClaudeTokens();
|
||||
const STATUS_STYLES: Record<string, { color: string; bg: string }> = {
|
||||
running: { color: c.status.success, bg: c.status.successBg },
|
||||
@@ -1443,7 +1449,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100%' }}>
|
||||
<ContextDrawer />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, overflow: 'hidden', ...(fullscreenChat && { maxWidth: FULLSCREEN_READING_MAX_W, width: '100%', mx: 'auto' }) }}>
|
||||
{!embedded && (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useEffect, type RefObject } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearTiledCard, setTiledCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { expandSession } from '@/shared/state/agentsSlice';
|
||||
import { clearTiledCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
|
||||
import DashboardHeader from './DashboardHeader';
|
||||
import TetherLayer from './TetherLayer';
|
||||
import DashboardCardLayer from './DashboardCardLayer';
|
||||
@@ -203,15 +202,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, [fullscreenCardId]);
|
||||
|
||||
// Arc fullscreen layout: the dock rail STAYS PUT (Arc's persistent sidebar) while the card
|
||||
// expands into the whole dashboard beside it; clicking a tile SWAPS which card owns the screen.
|
||||
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();
|
||||
@@ -273,33 +263,25 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{(
|
||||
<Box
|
||||
sx={fullscreenCardId ? {
|
||||
position: 'absolute', left: 0, top: 0, bottom: 0, width: 64, zIndex: 999995,
|
||||
display: 'flex', alignItems: 'center', pl: '4px',
|
||||
} : 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>
|
||||
{!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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{appsWindowOpen && !fullscreenCardId && (
|
||||
|
||||
@@ -933,7 +933,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
backdropFilter: 'blur(24px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(150%)',
|
||||
}),
|
||||
border: isSelected ? '2px solid #3b82f6' : '1px solid rgba(255,255,255,0.08)',
|
||||
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)',
|
||||
}),
|
||||
@@ -1189,6 +1189,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
sessionId={session.id}
|
||||
onClose={() => dispatch(collapseSession(session.id))}
|
||||
embedded
|
||||
fullscreenChat={isFullscreen}
|
||||
autoFocus={autoFocusInput}
|
||||
isGlowing={isGlowingRedux && !glowFading}
|
||||
onDismissGlow={dismissGlow}
|
||||
|
||||
@@ -23,8 +23,6 @@ export const TILE_ZONES: Record<string, { x: number; y: number; w: number; h: nu
|
||||
|
||||
// macOS Sequoia leaves a small gap between tiled windows; we match it.
|
||||
const GAP = 8;
|
||||
// Width the persistent dock rail owns while a card is fullscreen (the Arc sidebar analog).
|
||||
export const FULLSCREEN_DOCK_RAIL = 64;
|
||||
|
||||
export interface TiledStyle {
|
||||
left: number;
|
||||
@@ -48,16 +46,15 @@ function workspaceSize(): { w: number; h: number } {
|
||||
}
|
||||
|
||||
export function computeTiledStyle(zone: string, panX: number, panY: number, zoom: number): TiledStyle | null {
|
||||
// 'fullscreen' = the Arc layout: the dock rail stays put on the left and the card expands into
|
||||
// the ENTIRE dashboard beside it, floating on the same gap the tile zones use. Anchored to the
|
||||
// VIEWPORT (not the window) so docked chrome, like the pinned sidebar, squeezes the card instead
|
||||
// of being covered by it.
|
||||
// 'fullscreen' = the card expands into the ENTIRE dashboard, floating on the same gap the tile
|
||||
// zones use. Anchored to the VIEWPORT (not the window) so docked chrome, like the pinned sidebar,
|
||||
// squeezes the card instead of being covered by it (the canvas viewport shrinks with the sidebar).
|
||||
if (zone === 'fullscreen') {
|
||||
const { w: vpW, h: vpH } = workspaceSize();
|
||||
return {
|
||||
left: (FULLSCREEN_DOCK_RAIL + GAP - panX) / zoom,
|
||||
left: (GAP - panX) / zoom,
|
||||
top: (GAP - panY) / zoom,
|
||||
width: vpW - FULLSCREEN_DOCK_RAIL - GAP * 2,
|
||||
width: vpW - GAP * 2,
|
||||
height: vpH - GAP * 2,
|
||||
transform: `scale(${1 / zoom})`,
|
||||
transformOrigin: 'top left',
|
||||
|
||||
@@ -98,21 +98,6 @@ function DesktopDock({
|
||||
onAddNote,
|
||||
}: DesktopDockProps): React.ReactElement | null {
|
||||
const dispatch = useAppDispatch();
|
||||
const dockBodyRef = useRef<HTMLDivElement | null>(null);
|
||||
// macOS-dock fisheye: tiles swell as the cursor nears and neighbors fall off along the curve,
|
||||
// with a slight outward bulge so the column reads as a wheel (OptionWheel-style), not a ruler.
|
||||
const applyFisheye = useCallback((clientY: number | null) => {
|
||||
const root = dockBodyRef.current;
|
||||
if (!root) return;
|
||||
const rootTop = root.getBoundingClientRect().top;
|
||||
root.querySelectorAll<HTMLElement>('.osw-dock-tile').forEach((el) => {
|
||||
if (clientY == null) { el.style.transform = ''; return; }
|
||||
const cy = rootTop + el.offsetTop + el.offsetHeight / 2;
|
||||
const t = Math.max(0, 1 - Math.abs(clientY - cy) / 90);
|
||||
const emph = t * t;
|
||||
el.style.transform = `translateX(${(9 * emph).toFixed(1)}px) scale(${(1 + 0.5 * emph).toFixed(3)})`;
|
||||
});
|
||||
}, []);
|
||||
const [hovered, setHovered] = useState<{ id: string; top: number } | null>(null);
|
||||
const [liveShot, setLiveShot] = useState<{ id: string; dataUrl: string } | null>(null);
|
||||
const hoverTimer = useRef<number | null>(null);
|
||||
@@ -217,9 +202,7 @@ function DesktopDock({
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={dockBodyRef}
|
||||
onMouseMove={(e: React.MouseEvent) => applyFisheye(e.clientY)}
|
||||
onMouseLeave={() => { endHover(); applyFisheye(null); }}
|
||||
onMouseLeave={endHover}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 12,
|
||||
@@ -248,7 +231,6 @@ function DesktopDock({
|
||||
endHover();
|
||||
onFocusCard(entry.id, entry.rect);
|
||||
}}
|
||||
className="osw-dock-tile"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: TILE,
|
||||
@@ -261,9 +243,6 @@ function DesktopDock({
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
transition: 'transform 0.12s ease-out',
|
||||
willChange: 'transform',
|
||||
transformOrigin: 'left center',
|
||||
...(isActive && { outline: '2px solid #6aa2ff', outlineOffset: '2px' }),
|
||||
}}
|
||||
>
|
||||
@@ -296,7 +275,6 @@ function DesktopDock({
|
||||
<Box
|
||||
onClick={a.act}
|
||||
onMouseEnter={endHover}
|
||||
className="osw-dock-tile"
|
||||
sx={{
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
@@ -307,9 +285,6 @@ function DesktopDock({
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
transition: 'transform 0.12s ease-out',
|
||||
willChange: 'transform',
|
||||
transformOrigin: 'left center',
|
||||
}}
|
||||
>
|
||||
{a.icon}
|
||||
@@ -320,7 +295,6 @@ function DesktopDock({
|
||||
<Box
|
||||
onClick={() => dispatch(openSettingsModal(undefined))}
|
||||
onMouseEnter={endHover}
|
||||
className="osw-dock-tile"
|
||||
sx={{
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
@@ -331,9 +305,6 @@ function DesktopDock({
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
transition: 'transform 0.12s ease-out',
|
||||
willChange: 'transform',
|
||||
transformOrigin: 'left center',
|
||||
}}
|
||||
>
|
||||
<SettingsIcon sx={{ fontSize: 18, color: '#e8e8ee' }} />
|
||||
@@ -341,7 +312,6 @@ function DesktopDock({
|
||||
<Box
|
||||
onClick={onApplications}
|
||||
onMouseEnter={endHover}
|
||||
className="osw-dock-tile"
|
||||
sx={{
|
||||
width: TILE,
|
||||
height: TILE,
|
||||
@@ -352,9 +322,6 @@ function DesktopDock({
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
transition: 'transform 0.12s ease-out',
|
||||
willChange: 'transform',
|
||||
transformOrigin: 'left center',
|
||||
}}
|
||||
>
|
||||
<AppsRoundedIcon sx={{ fontSize: 18, color: '#e8e8ee' }} />
|
||||
|
||||
Reference in New Issue
Block a user