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
21 changes: 10 additions & 11 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useSessionList } from '@/state/useSessionList';
import { useApprovalsQueue } from '@/state/useApprovalsQueue';
import { useAgentDefinitions } from '@/state/useAgentDefinitions';
import { useSessionFull } from '@/state/useSessionFull';
import { MonitorRail } from '@/monitors/MonitorRail';

const UI_VERSION = 'v2.0.0-rc1';
const RUNTIME_VERSION_FALLBACK = 'unknown';
Expand All @@ -29,14 +30,6 @@ const paneStyle: CSSProperties = {
minHeight: 0,
};

const monitorRailPlaceholder: CSSProperties = {
background: 'var(--bg-page)',
borderLeft: '1px solid var(--hair)',
padding: 16,
fontSize: 11,
color: 'var(--ink-3)',
};

export function App() {
const [activeSid, setActiveSid] = useState<string | null>(null);

Expand Down Expand Up @@ -87,9 +80,15 @@ export function App() {
onSelect={setActiveSid}
/>
<SessionCanvas activeSid={activeSid} />
<div style={monitorRailPlaceholder}>
Ambient monitors (Phase 5)
</div>
<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}
Expand Down
44 changes: 44 additions & 0 deletions web/src/monitors/MonitorRail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { CSSProperties } from 'react';
import type { SessionSummary } from '@/state/useSessionList';
import type { AgentDefinition, ToolCall } from '@/api/types';
import { SelectedPanel } from './SelectedPanel';
import { OtherSessionsPanel } from './OtherSessionsPanel';
import { ApprovalsQueuePanel } from './ApprovalsQueuePanel';
import { LessonsPanel } from './LessonsPanel';
import { ToolsPanel } from './ToolsPanel';
import { HealthPanel } from './HealthPanel';

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

const wrap: CSSProperties = {
width: 340,
display: 'flex',
flexDirection: 'column',
background: 'var(--bg-page)',
borderLeft: '1px solid var(--hair)',
overflowY: 'auto',
minHeight: 0,
};

export function MonitorRail({
sessions, activeSid, queue, agentsByName, toolCalls, sessionId, onSelectSession,
}: MonitorRailProps) {
return (
<aside style={wrap}>
<SelectedPanel agentsByName={agentsByName} toolCalls={toolCalls} />
<OtherSessionsPanel sessions={sessions} activeSid={activeSid} onSelect={onSelectSession} />
<ApprovalsQueuePanel queue={queue} onSelect={onSelectSession} />
<LessonsPanel sessionId={sessionId} />
<ToolsPanel />
<HealthPanel />
</aside>
);
}
10 changes: 10 additions & 0 deletions web/tests/component/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ describe('<App>', () => {
status: 200, headers: { 'content-type': 'application/json' },
}));
}
if (url.includes('/tools') || url.includes('/lessons')) {
return Promise.resolve(new Response(JSON.stringify([]), {
status: 200, headers: { 'content-type': 'application/json' },
}));
}
if (url.includes('/health')) {
return Promise.resolve(new Response(JSON.stringify({ status: 'ok' }), {
status: 200, headers: { 'content-type': 'application/json' },
}));
}
return Promise.resolve(new Response('{}', { status: 200 }));
});
// @ts-expect-error -- jsdom does not provide EventSource
Expand Down
48 changes: 48 additions & 0 deletions web/tests/component/MonitorRail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen } from '../_helpers/render';
import { MonitorRail } from '@/monitors/MonitorRail';
import type { SessionSummary } from '@/state/useSessionList';
import type { AgentDefinition, ToolCall } from '@/api/types';

const sessions: SessionSummary[] = [
{ id: 'SES-1', status: 'in_progress', label: 'A', created_at: 't0', updated_at: 't1' },
];
const queue: SessionSummary[] = [];
const agents: Record<string, AgentDefinition> = {};
const tools: ToolCall[] = [];

describe('<MonitorRail>', () => {
beforeEach(() => {
global.fetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } }),
);
});

it('renders all 6 panel titles', () => {
render(
<MonitorRail
sessions={sessions} activeSid={null} queue={queue}
agentsByName={agents} toolCalls={tools} sessionId={null}
onSelectSession={() => {}}
/>,
);
expect(screen.getByText(/Selected/)).toBeInTheDocument();
expect(screen.getByText(/Other Sessions/)).toBeInTheDocument();
expect(screen.getByText(/Approvals Queue/)).toBeInTheDocument();
expect(screen.getByText(/Lessons/)).toBeInTheDocument();
expect(screen.getByText(/Tool Catalog/)).toBeInTheDocument();
expect(screen.getByText(/System Health/)).toBeInTheDocument();
});

it('renders pinned panels open by default (Selected, Others, Approvals)', () => {
render(
<MonitorRail
sessions={sessions} activeSid={null} queue={queue}
agentsByName={agents} toolCalls={tools} sessionId={null}
onSelectSession={() => {}}
/>,
);
// Selected pinned with empty body → "Click an agent..."
expect(screen.getByText(/Click an agent, tool, or message/i)).toBeInTheDocument();
});
});
Loading