From 882e123d432f259e2bea594cc447b9fda5ac3a34 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 16 May 2026 05:15:07 +0000 Subject: [PATCH] =?UTF-8?q?feat(web):=20wire=20shell=20into=20=20?= =?UTF-8?q?=E2=80=94=20first=20end-to-end=20render?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composes the Phase 3 shell components (Topbar, FlowStrip, SessionsRail, Statusbar) and Phase 2 data hooks (useUiHints, useSessionList, useApprovalsQueue, useAgentDefinitions, useSessionFull) into a working that renders the empty 3-pane shell. Canvas is an empty-state placeholder (Phase 4); monitor rail is a placeholder (Phase 5). main.tsx now wraps in QueryClientProvider + SelectedRefProvider and mounts the IconSprite. vite.config.ts gains the same '@/' alias as vitest.config.ts (build broke without it now that main/App use '@/'). Tests: 4 new App.test.tsx cases (brand name from useUiHints, empty SessionsRail, Statusbar version, canvas empty-state) — full suite remains green at 91 passing. Co-Authored-By: Claude Opus 4.7 --- web/src/App.tsx | 139 ++++++++++++++++++++++++------- web/src/main.tsx | 16 +++- web/tests/component/App.test.tsx | 66 +++++++++++++++ web/vite.config.ts | 6 ++ 4 files changed, 197 insertions(+), 30 deletions(-) create mode 100644 web/tests/component/App.test.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index c53cffa..8fc845b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,33 +1,116 @@ -export default function App() { +import { useState } from 'react'; +import type { CSSProperties } from 'react'; +import { Topbar, type Health } from '@/shell/Topbar'; +import { Statusbar, type ConnectionState, type VmSeqState } from '@/shell/Statusbar'; +import { SessionsRail } from '@/shell/SessionsRail'; +import { FlowStrip } from '@/shell/FlowStrip'; +import { useUiHints } from '@/state/useUiHints'; +import { useSessionList } from '@/state/useSessionList'; +import { useApprovalsQueue } from '@/state/useApprovalsQueue'; +import { useAgentDefinitions } from '@/state/useAgentDefinitions'; +import { useSessionFull } from '@/state/useSessionFull'; + +const UI_VERSION = 'v2.0.0-rc1'; +const RUNTIME_VERSION_FALLBACK = 'unknown'; + +const shellStyle: CSSProperties = { + display: 'grid', + gridTemplateRows: 'auto auto 1fr auto', + height: '100vh', + background: 'var(--bg-page)', + color: 'var(--ink-1)', + fontFamily: 'var(--ff-sans)', +}; + +const paneStyle: CSSProperties = { + display: 'grid', + gridTemplateColumns: '220px 1fr 340px', + minHeight: 0, +}; + +const canvasEmptyStyle: CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: 'var(--bg-elev)', + borderRight: '1px solid var(--hair)', + padding: 32, +}; + +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(null); + + const uiHints = useUiHints(); + const sessionList = useSessionList(); + const approvals = useApprovalsQueue(); + const agents = useAgentDefinitions(); + const sessionFull = useSessionFull(activeSid); + + const brandName = uiHints.data?.brand_name ?? 'ASR'; + const envName = uiHints.data?.environments?.[0] ?? 'dev'; + const appName = 'runtime'; + + const health: Health = + sessionList.error || approvals.error || agents.isError + ? 'down' + : 'ok'; + + const connection: ConnectionState = + sessionList.error || (sessionFull.error && activeSid !== null) + ? 'disconnected' + : 'connected'; + + const vmSeqState: VmSeqState = 'in-sync'; + const vmSeq = sessionFull.state.vmSeq; + return ( -
-

- ASR Operator Console -

-

- v2.0.0-rc1 · scaffold + design tokens · components land in tasks 16-20 -

-
- Token preview: warm cream #FBFAF6 page, - accent navy #2A4365, - deep ink #15110A. +
+ {/* Phase 6: open search overlay */}} + onNew={() => {/* Phase 6: open NewSessionModal */}} + onApprovalsClick={() => {/* Phase 6: open approvals view */}} + /> + +
+ +
+ + Select a session from the rail or create a new one. + +
+
+ Ambient monitors (Phase 5) +
+
); } diff --git a/web/src/main.tsx b/web/src/main.tsx index 53662f4..632d531 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,10 +1,22 @@ import './styles/global.css'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { SelectedRefProvider } from '@/state/selectedRef'; +import { IconSprite } from '@/icons/sprite'; +import { App } from './App'; + +const queryClient = new QueryClient({ + defaultOptions: { queries: { refetchOnWindowFocus: false } }, +}); createRoot(document.getElementById('root')!).render( - + + + + + + , ); diff --git a/web/tests/component/App.test.tsx b/web/tests/component/App.test.tsx new file mode 100644 index 0000000..9c21221 --- /dev/null +++ b/web/tests/component/App.test.tsx @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '../_helpers/render'; +import { App } from '@/App'; + +const uiHints = { + brand_name: 'Test Brand', + brand_logo_url: null, + approval_rationale_templates: [], + hitl_question_templates: {}, + environments: ['dev'], +}; + +const sessions: unknown[] = []; +const agents: unknown[] = []; + +describe('', () => { + beforeEach(() => { + global.fetch = vi.fn().mockImplementation((url: string) => { + if (url.includes('/config/ui-hints')) { + return Promise.resolve(new Response(JSON.stringify(uiHints), { + status: 200, headers: { 'content-type': 'application/json' }, + })); + } + if (url.includes('/sessions') && !url.includes('/recent/events') && !url.includes('/events')) { + return Promise.resolve(new Response(JSON.stringify(sessions), { + status: 200, headers: { 'content-type': 'application/json' }, + })); + } + if (url.includes('/agents')) { + return Promise.resolve(new Response(JSON.stringify(agents), { + status: 200, headers: { 'content-type': 'application/json' }, + })); + } + return Promise.resolve(new Response('{}', { status: 200 })); + }); + // @ts-expect-error -- jsdom does not provide EventSource + global.EventSource = class { + constructor(public url: string) {} + onopen: ((e: Event) => void) | null = null; + onmessage: ((e: MessageEvent) => void) | null = null; + onerror: ((e: Event) => void) | null = null; + close() {} + addEventListener() {} + }; + }); + + it('renders the brand name from useUiHints once loaded', async () => { + render(); + await waitFor(() => expect(screen.getByText('Test Brand')).toBeInTheDocument()); + }); + + it('renders an empty-state SessionsRail when no sessions', async () => { + render(); + await waitFor(() => expect(screen.getByText(/No sessions yet/i)).toBeInTheDocument()); + }); + + it('renders Statusbar with version', async () => { + render(); + await waitFor(() => expect(screen.getByText(/ui v/)).toBeInTheDocument()); + }); + + it('renders the canvas empty state when no session selected', async () => { + render(); + await waitFor(() => expect(screen.getByText(/Select a session/i)).toBeInTheDocument()); + }); +}); diff --git a/web/vite.config.ts b/web/vite.config.ts index 59b08f2..867db0b 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,9 +1,15 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; +import { fileURLToPath } from 'node:url'; export default defineConfig({ plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, server: { port: 5173, strictPort: true,