Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { useAgentDefinitions } from '@/state/useAgentDefinitions';
import { useSessionFull } from '@/state/useSessionFull';
import { MonitorRail } from '@/monitors/MonitorRail';
import { NewSessionModal } from '@/modals/NewSessionModal';
import { TabletShell } from '@/shell/TabletShell';
import { MobileShell } from '@/shell/MobileShell';
import { useBreakpoint } from '@/state/useBreakpoint';

const UI_VERSION = 'v2.0.0-rc1';
const RUNTIME_VERSION_FALLBACK = 'unknown';
Expand Down Expand Up @@ -40,6 +43,7 @@ export function App() {
const approvals = useApprovalsQueue();
const agents = useAgentDefinitions();
const sessionFull = useSessionFull(activeSid);
const breakpoint = useBreakpoint();

const brandName = uiHints.data?.brand_name ?? 'ASR';
const envName = uiHints.data?.environments?.[0] ?? 'dev';
Expand Down Expand Up @@ -75,23 +79,43 @@ export function App() {
activeAgent={null}
graphVersion={`v${agents.data?.list.length ?? 0}`}
/>
<div style={paneStyle}>
<SessionsRail
{breakpoint === 'mobile' ? (
<MobileShell
sessions={sessionList.sessions}
activeSid={activeSid}
onSelect={setActiveSid}
onSelectSession={setActiveSid}
queue={approvals.queue}
agentsByName={agents.data?.byName ?? {}}
toolCalls={sessionFull.state.toolCalls}
/>
<SessionCanvas activeSid={activeSid} />
<MonitorRail
) : breakpoint === 'tablet' ? (
<TabletShell
sessions={sessionList.sessions}
activeSid={activeSid}
onSelectSession={setActiveSid}
queue={approvals.queue}
agentsByName={agents.data?.byName ?? {}}
toolCalls={sessionFull.state.toolCalls}
sessionId={activeSid}
onSelectSession={setActiveSid}
/>
</div>
) : (
<div style={paneStyle}>
<SessionsRail
sessions={sessionList.sessions}
activeSid={activeSid}
onSelect={setActiveSid}
/>
<SessionCanvas activeSid={activeSid} />
<MonitorRail
sessions={sessionList.sessions}
activeSid={activeSid}
queue={approvals.queue}
agentsByName={agents.data?.byName ?? {}}
toolCalls={sessionFull.state.toolCalls}
sessionId={activeSid}
onSelectSession={setActiveSid}
/>
</div>
)}
<Statusbar
connection={connection}
sseEventCount={sessionFull.state.events.length}
Expand Down
85 changes: 85 additions & 0 deletions web/src/shell/MobileSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { CSSProperties, ReactNode } from 'react';
import * as Dialog from '@radix-ui/react-dialog';

interface MobileSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: ReactNode;
testId?: string;
}

const overlay: CSSProperties = {
position: 'fixed',
inset: 0,
background: 'rgba(21,17,10,0.30)',
backdropFilter: 'blur(2px)',
zIndex: 1000,
};

const content: CSSProperties = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
height: 'min(85vh, 720px)',
background: 'var(--bg-page)',
borderTop: '1px solid var(--hair-strong)',
borderRadius: 0,
zIndex: 1001,
display: 'flex',
flexDirection: 'column',
boxShadow: 'var(--e-3)',
animation: 'asr-sheet-slide-up 220ms cubic-bezier(0.16, 1, 0.3, 1)',
};

const handle: CSSProperties = {
width: 44, height: 4,
background: 'var(--ink-4)',
margin: '8px auto 4px',
opacity: 0.35,
};

const titleRow: CSSProperties = {
height: 40, padding: '0 16px',
display: 'flex', alignItems: 'center',
borderBottom: '1px solid var(--hair)',
};

export function MobileSheet({ open, onOpenChange, title, children, testId }: MobileSheetProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay style={overlay} />
<Dialog.Content style={content} data-mobile-sheet={testId ?? ''}>
<div style={handle} aria-hidden />
<div style={titleRow}>
<Dialog.Title
style={{
margin: 0,
fontSize: 10,
fontFamily: 'var(--ff-mono)',
letterSpacing: '0.14em',
textTransform: 'uppercase',
color: 'var(--ink-3)',
flex: 1,
}}
>
{title}
</Dialog.Title>
<Dialog.Close
aria-label="Close"
style={{
width: 28, height: 28,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
background: 'transparent', border: 'none', color: 'var(--ink-3)',
cursor: 'pointer', fontSize: 18, lineHeight: 1,
}}
>×</Dialog.Close>
</div>
<div style={{ flex: 1, overflow: 'auto', minHeight: 0 }}>{children}</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
126 changes: 126 additions & 0 deletions web/src/shell/MobileShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useState } from 'react';
import type { CSSProperties } from 'react';
import { SessionsRail } from '@/shell/SessionsRail';
import { SessionCanvas } from '@/canvas/SessionCanvas';
import { MonitorRail } from '@/monitors/MonitorRail';
import { MobileSheet } from '@/shell/MobileSheet';
import type { SessionSummary } from '@/state/useSessionList';
import type { AgentDefinition, ToolCall } from '@/api/types';

interface MobileShellProps {
sessions: SessionSummary[];
activeSid: string | null;
onSelectSession: (sid: string) => void;
queue: SessionSummary[];
agentsByName: Record<string, AgentDefinition>;
toolCalls: ToolCall[];
}

const shellWrap: CSSProperties = {
display: 'grid',
gridTemplateRows: '1fr auto',
minHeight: 0,
};

const tabBar: CSSProperties = {
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
borderTop: '1px solid var(--hair-strong)',
background: 'var(--bg-elev)',
height: 56,
};

const tabBtn = (active: boolean): CSSProperties => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'var(--ff-mono)',
fontSize: 11,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: active ? 'var(--ink-1)' : 'var(--ink-3)',
background: 'transparent',
border: 'none',
borderTop: active ? '2px solid var(--acc)' : '2px solid transparent',
cursor: 'pointer',
padding: 0,
});

type ActiveSheet = 'sessions' | 'monitors' | null;

export function MobileShell({
sessions, activeSid, onSelectSession, queue, agentsByName, toolCalls,
}: MobileShellProps) {
const [sheet, setSheet] = useState<ActiveSheet>(null);

function selectAndClose(sid: string) {
onSelectSession(sid);
setSheet(null);
}

return (
<div style={shellWrap} data-shell="mobile">
<div style={{ overflow: 'auto', minHeight: 0 }}>
<SessionCanvas activeSid={activeSid} />
</div>
<nav style={tabBar} aria-label="Mobile navigation">
<button
type="button"
style={tabBtn(sheet === 'sessions')}
onClick={() => setSheet(sheet === 'sessions' ? null : 'sessions')}
aria-label="Open sessions"
aria-pressed={sheet === 'sessions'}
>
Sessions
</button>
<button
type="button"
style={tabBtn(sheet === null)}
onClick={() => setSheet(null)}
aria-label="Show canvas"
aria-pressed={sheet === null}
>
Canvas
</button>
<button
type="button"
style={tabBtn(sheet === 'monitors')}
onClick={() => setSheet(sheet === 'monitors' ? null : 'monitors')}
aria-label="Open monitors"
aria-pressed={sheet === 'monitors'}
>
Monitors
</button>
</nav>
<MobileSheet
open={sheet === 'sessions'}
onOpenChange={(o) => setSheet(o ? 'sessions' : null)}
title="Sessions"
testId="sessions"
>
<SessionsRail
sessions={sessions}
activeSid={activeSid}
onSelect={selectAndClose}
/>
</MobileSheet>
<MobileSheet
open={sheet === 'monitors'}
onOpenChange={(o) => setSheet(o ? 'monitors' : null)}
title="Monitors"
testId="monitors"
>
<MonitorRail
sessions={sessions}
activeSid={activeSid}
queue={queue}
agentsByName={agentsByName}
toolCalls={toolCalls}
sessionId={activeSid}
onSelectSession={selectAndClose}
/>
</MobileSheet>
</div>
);
}
112 changes: 112 additions & 0 deletions web/src/shell/TabletShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useState } from 'react';
import type { CSSProperties } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { SessionsRail } from '@/shell/SessionsRail';
import { SessionCanvas } from '@/canvas/SessionCanvas';
import { MonitorRail } from '@/monitors/MonitorRail';
import type { SessionSummary } from '@/state/useSessionList';
import type { AgentDefinition, ToolCall } from '@/api/types';

interface TabletShellProps {
sessions: SessionSummary[];
activeSid: string | null;
onSelectSession: (sid: string) => void;
queue: SessionSummary[];
agentsByName: Record<string, AgentDefinition>;
toolCalls: ToolCall[];
}

const grid: CSSProperties = {
display: 'grid',
gridTemplateColumns: '180px 1fr',
minHeight: 0,
position: 'relative',
};

const monitorsBtn: CSSProperties = {
position: 'absolute',
top: 12,
right: 12,
height: 28,
padding: '0 12px',
fontFamily: 'var(--ff-mono)',
fontSize: 11,
letterSpacing: '0.06em',
textTransform: 'uppercase',
color: 'var(--ink-1)',
background: 'var(--bg-elev)',
border: '1px solid var(--hair-strong)',
borderRadius: 0,
cursor: 'pointer',
zIndex: 5,
};

const sheetContent: CSSProperties = {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
width: 'min(360px, 90vw)',
background: 'var(--bg-page)',
borderLeft: '1px solid var(--hair-strong)',
boxShadow: 'var(--e-3)',
overflow: 'auto',
zIndex: 1000,
display: 'flex',
flexDirection: 'column',
};

export function TabletShell({
sessions, activeSid, onSelectSession, queue, agentsByName, toolCalls,
}: TabletShellProps) {
const [monitorsOpen, setMonitorsOpen] = useState(false);
return (
<div style={grid} data-shell="tablet">
<SessionsRail
sessions={sessions}
activeSid={activeSid}
onSelect={onSelectSession}
/>
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<button
type="button"
aria-label="Open monitors"
style={monitorsBtn}
onClick={() => setMonitorsOpen(true)}
>
Monitors
</button>
<SessionCanvas activeSid={activeSid} />
</div>
<Dialog.Root open={monitorsOpen} onOpenChange={setMonitorsOpen}>
<Dialog.Portal>
<Dialog.Overlay style={{
position: 'fixed', inset: 0,
background: 'rgba(21,17,10,0.18)',
backdropFilter: 'blur(2px)',
zIndex: 999,
}} />
<Dialog.Content style={sheetContent} data-monitors-sheet="">
<Dialog.Title style={{
fontSize: 10, color: 'var(--ink-3)',
letterSpacing: '0.14em', textTransform: 'uppercase',
padding: '12px 16px', borderBottom: '1px solid var(--hair)',
margin: 0, fontFamily: 'var(--ff-mono)',
}}>
Monitors
</Dialog.Title>
<MonitorRail
sessions={sessions}
activeSid={activeSid}
queue={queue}
agentsByName={agentsByName}
toolCalls={toolCalls}
sessionId={activeSid}
onSelectSession={(sid) => { onSelectSession(sid); setMonitorsOpen(false); }}
/>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
);
}
Loading
Loading