13-ai-condorwave: W6filled17 citations

Condor Web Surface — the ddx-web Frontend for the Condor Engine

Audiences: developer, internal

Condor Web Surface — the ddx-web Frontend for the Condor Engine

The browser half of Condor. Roughly 303 files across six coherent top-level directories in ddx-web/src turn the engine's AG-UI/SSE frames (see Condor SSE Pump & AG-UI) into a chat surface, a run inspector, tool/entity cards, HITL approval prompts, and a sandbox terminal + file workspace. This is the consumer side of the AG-UI channel-name contract — the single trap most likely to make a tsc-green, lint-green change render an empty panel.

Purpose

Condor's engine (Condor Engine) emits an AI-SDK UI Message Stream for chat text and a parallel raw RFC 8895 AG-UI frame stream for everything structural (plan, steps, tool lifecycle, cards, suspends, sub-agents). The web surface exists to keep those two streams separate but coherent: chat text renders through the AI-SDK passthrough path while AG-UI frames fold into a derived run state that drives the inspector, the tool timeline, the approval prompts, and the workspace panels. The clinician sees one conversation; the surface is really two subscriptions reconciled by a pure reducer.

The six top-level directories

DirectoryFilesRole
app/[locale]/portal/doctor/(with-nav)/condor/routeThe doctor-facing chat route: page.tsx, a chat/ subroute, and in-route _components/ still being lifted into the module.
app/[locale]/portal/(shared)/condor-tools/5Cross-portal tool-admin surface (page.tsx, CondorToolsClient.tsx, api.ts, logic.ts, types.ts) over the tool catalog.
app/api/condor/18 routesBFF Route Handlers proxying ddx-api's condor.controller.tschat/[id] plus agui, stream, approve, resume, stop/[runId], resubmit/[msgId], active-run, timelapse, ui-action, files, pinned-cards, attachments, and top-level sessions / models / settings / transcribe-token.
app/api/condor-tools/3 routescatalog, health, probe — the tool-admin BFF.
components/ddx/condor/~145The UI module: CondorProvider.tsx (headless core), cards/ (21), renderers/ (42), shell/ (70, incl. ComputerPanel/ + RunSidePanel/), hooks/ (8), types.ts, variants.ts.
lib/condor/~48Stream + state logic: use-ag-ui-stream.ts, use-ai-chat.ts, agui-run-state-fold.ts, agui-artifacts.ts, agui-types.ts, approval-mapping.ts, capability-lifecycle.ts, rehydrate-run-events.ts, plus preview renderers and small hooks.

No split is warranted — the six dirs are non-overlapping, and the module/lib boundary is deliberate: lib/condor/ holds framework-free logic and the stream seam, components/ddx/condor/ holds React.

CondorProvider — the headless core

components/ddx/condor/CondorProvider.tsx (596 lines) was carved out of an 842-line ChatPanel.client.tsx god-component. It owns session/run/message state, dispatch, the injectable logger, the callback fan-out, and — critically — both SSE subscriptions:

  • useAiChat (lib/condor/use-ai-chat.ts) — the AI-SDK chat turn path.
  • useAgUiStream (lib/condor/use-ag-ui-stream.ts) — the AG-UI inspector path.

The provider header marks these as a DESIGN §18 justified exception that is NEVER collapsed (CondorProvider.tsx:1-20): one eventFilter predicate wraps both streams, but they remain two connections. Collapsing them would put structural frames on the chat render path.

What the provider deliberately does not own: layout/JSX (the shell composes panels around it), the persisted Zustand useCondorMode store (it reads, never replaces), and leaf-local upload/attachment state — it exposes only send, closing over host-supplied attachment getters.

Rendering diagram…

Frames are projected to UI state by useDerivedRunState (components/ddx/condor/hooks/use-derived-run-state.ts) over the pure applyCondorEvent reducer, yielding CondorRunState + a session rollup.

useAgUiStream — the scoped SSE consumer

lib/condor/use-ag-ui-stream.ts (367 lines) is the SSE isolation seam: components (RunSidePanel, AguiInspectorPanel) never open their own connection — they receive an aguiEventsByRun map by prop-drilling (use-ag-ui-stream.ts:19-24). Three design facts matter when touching it:

  1. fetch + ReadableStream, not EventSource. The browser EventSource API cannot send custom request headers and always issues a bare GET. Using fetch with a manual SSE line-parser threads an AbortController for clean teardown on runId change/unmount and lets the hook pass its own Last-Event-ID on reconnect (use-ag-ui-stream.ts:26-33).
  2. Bounded reconnect. Exponential backoff from 1 s capped at 15 s, parked after 5 consecutive failures so a genuinely dead run cannot hot-loop; a visibilitychange back to visible re-arms with a fresh budget. Terminal frames (RunFinished/RunFailed) and deliberate teardown stop the loop permanently (use-ag-ui-stream.ts:41-56).
  3. Non-retryable statuses stop immediately400, 401, 403, 404, 410 are fatal, not retried (use-ag-ui-stream.ts:71). A silent BFF 403 therefore surfaces as a dead inspector, not a retry storm.

Per-run event retention is capped at MAX_EVENTS_PER_RUN = 2000 (use-ag-ui-stream.ts:40). The stream target is the BFF live-bridge proxy app/api/condor/chat/[id]/agui/route.ts, which resolves the NextAuth session to a Bearer server-side — the browser never reaches ddx-api:6100 directly.

The AG-UI channel-name reality (consumer side)

This is the trap. The mapper on the emitter side (Condor SSE Pump & AG-UI) has two paths, and consumers here subscribe to the projected condor.* name, not the wire data-* type:

  • Explicitly-cased frames — the existing tool-emitter set arrives as data-condor-tool-started / -progress / -completed / -failed / -card etc. and is hand-mapped by an explicit case block to condor.tool.*. Consumers subscribe to the projected names: condor.tool.started, condor.tool.progress, condor.tool.completed, condor.tool.failed, plus condor.task.* and condor.run.* (lib/condor/agui-run-state-fold.ts:111-232, :310-323). For these frames data-condor- on the wire is correct.
  • Fallthrough frames — any new, un-cased data-<suffix> is projected verbatim as condor.<suffix>; the condor- segment is not stripped. So a new frame emitted as data-condor-foo doubles to condor.condor-foo, and a consumer subscribing to condor.foo renders empty — tsc, lint, and grep all stay green; the failure is browser-only.

Live proof that this already happened once: condor.condor-run is a real, surviving mis-prefixed channel with its own descriptor case in lib/condor/agui-artifacts.ts:443-451 (the run-state envelope carrying type + planRevision). It is documented, not a defect to "fix" — renaming it would break the emitter. The sibling condor.subagent-* names (lib/condor/agui-types.ts:215, :240-242) are the same generic-fallthrough class, correctly single-prefixed.

Rendering diagram…

Rule when adding a frame: an existing tool frame uses data-condor-* (it has an explicit case); a NEW frame on the fallthrough path must be data-<suffix> with no condor-, or it double-prefixes.

agui-artifacts.ts is total over the AG-UI union — its default branch shows the raw event name rather than falling through to a bare badge (agui-artifacts.ts:455-460), so a mis-prefixed frame appears in the inspector as an unmapped row instead of vanishing. That is the cheapest live diagnostic for a prefix mistake.

Tool-part, card, and artifact rendering

Rendering is registry-driven. lib/condor/renderer-registry.tsx is now a re-export shim (118 lines) — the canonical registry moved to components/ddx/condor/renderers/registry so the import chain points module→module instead of registry→route folder (renderer-registry.tsx:1-16). The shim survives only for in-route _components/ consumers (MessageBubble, OmniContent, FilePreview) still awaiting the lift, plus the module's own UiBlockRenderer, whose import of getRenderer is a documented circular dep resolved lazily. New code should import from the module registry, not the shim.

Three render families sit on top:

  • components/ddx/condor/cards/ (21 files) — clinical/domain result cards, each paired with a .types.ts: AnamnesisCard, CompareCard, DiagnosisOutcomeCard, FindingsCard, FindResultCard, MatcherResultCard and siblings.
  • components/ddx/condor/renderers/ (42 files) — generic tool/UI-block renderers: ApprovalCard, CondorSuspendCard, DocumentCard, EntityTable, CitationList, CompletedStepsAccordion, AttachmentPreviewCard, InlineDiagnosisOps, CardPinButton, FilePreview/FilePreviewProvider.
  • lib/condor/ preview blocks — CodeFenceRenderers.tsx, HtmlPreviewBlock.tsx, SvgPreviewBlock.tsx render model-emitted fenced content, plus ui-block-spec.ts / ui-block-reconcile.ts for block identity and reconciliation across re-renders.

HITL prompts are wired by lib/condor/approval-mapping.ts (158 lines) → ApprovalCard / CondorSuspendCard / SuspendMount.client.tsx, posting back through app/api/condor/chat/[id]/approve/route.ts and resume/route.ts. See Condor HITL Approval for the policy side — the surface renders the suspend frame; it does not decide gating.

lib/condor/capability-lifecycle.ts renders the plan-mutation and capability-search lifecycle (condor.plan.mutation.proposed / .approval.required / .applied / .rejected, condor.capability.search.*, condor.capability.candidate.surfaced, condor.capability.authorization.deniedcapability-lifecycle.ts:56-166).

Shell, run inspector, terminal, and workspace

components/ddx/condor/shell/ (70 files) composes the surface:

  • Chat chromeChatComposer.tsx + ChatComposerControls.tsx, CondorChatHeader.tsx, DispatchPicker.tsx (selects the dispatch that the engine's flow router resolves), CondorModePills.tsx, CondorSettingsPopover.tsx, QuickActions.tsx, WelcomeState.tsx, VoiceControls.tsx + useInternalVoice.ts.
  • Session railSessionsSidebar.tsx, SessionsList.client.tsx, SessionRow.tsx, EditableSessionTitle.tsx, RenameSessionDialog.tsx, NewSessionButton.tsx.
  • Run inspectorRunSidePanel.tsx + RunSidePanel/, RunPanel.tsx, RunStatusPill.tsx, RunTimer.tsx, UsageBadge.tsx, TranscriptStream.tsx, ErrorFrameBanner.tsx, BannerStack.client.tsx.
  • Entity linkageCondorEntityLinkBar.tsx, CondorEntityLinkPicker.tsx, entity-link-loaders.ts, entity-link-types.ts bind the session to a patient/visit.
  • RehydrationRehydratePlanSeed.client.tsx and RehydrateRunStateSeed.client.tsx seed the reducer from persisted run events (lib/condor/rehydrate-run-events.ts) so a page reload restores plan + run state instead of an empty inspector.
  • Sandbox workspaceshell/ComputerPanel/ is the Condor sandbox UI: ComputerPanel.tsx + header, FileTreePane.tsx + file-tree-ops.ts, FileViewer.tsx, TaskProgressList.tsx, StatusPill.tsx, and the terminal (TerminalView.tsxXtermClient.tsx, themed by terminal-theme.ts). WorkspaceArtifactCard.tsx surfaces produced workspace files back into the conversation.

The terminal is the one place the browser dials ddx-api directly: the BFF returns a PTY hint { wsUrl, token } and the dynamically-imported xterm.js client opens a direct browser→ddx-api WebSocket, because Next.js cannot proxy WS (ComputerPanel/TerminalView.tsx:4-5). It is the second sanctioned §18 scoped-connection exception alongside useAgUiStream. TerminalView must not dial when auth is unavailable — a doomed dial was the source of a prior scary error state (TerminalView.tsx:91); XtermClient owns a StrictMode-safe terminal + socket lifecycle so a stale term.onData cannot pump bytes into a rotated socket (XtermClient.tsx:28-32).

Operational notes

  • Browser never calls ddx-api directly — except the PTY WebSocket. Every HTTP round-trip goes through app/api/condor*/ Route Handlers, which resolve the session to a Bearer server-side. A missing BFF role entry yields a silent 403 → an empty panel with no api.log entry. The sandbox terminal is the sole carve-out (WS is unproxyable) and is token-gated by the BFF hint.
  • Styling uses semantic Toucan tokens onlybg-card, text-muted-foreground, bg-primary/10, and the --status-* family. No raw palette classes, no card borders.
  • Two streams, one filter, never collapsed — the §18 exception is documented in the provider header; a "simplification" that merges them is a regression.
  • Verify AG-UI changes in the browser. Channel-name mistakes are invisible to pnpm typecheck / pnpm lint / grep. The inspector's unmapped row (from agui-artifacts.ts's total default) is the fastest confirmation that a frame arrived under an unexpected name.
  • Frontmatter/KB gatecd ddx-web && pnpm kb:validate (scripts/kb-validate-frontmatter.ts) must pass before commit.
  • Condor SSE Pump & AG-UI — the emitter side of the frame contract this surface consumes.
  • Condor Engine — the runtime, flow router, and the chat/AG-UI transport endpoints the BFF proxies.
  • TUCAN UI — the legacy sibling surface (ddx-web/src/features/tucan/) that Condor's UI supersedes per-flow.
  • Condor HITL Approval — the policy side of the approval/suspend prompts rendered here.