[eric] desktop: full size view reads like a chat page (centered reading column), drop the dock rail + fisheye

This commit is contained in:
ciregenz
2026-07-21 13:44:23 -07:00
parent cd07b2143f
commit cd6add56b1
5 changed files with 36 additions and 83 deletions
@@ -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' }} />