Condor Web Surface — the ddx-web Frontend for the Condor Engine
Audiences: developer, internal
The browser half of Condor. Roughly 303 files across six coherent top-level directories in
ddx-web/srcturn 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
| Directory | Files | Role |
|---|---|---|
app/[locale]/portal/doctor/(with-nav)/condor/ | route | The 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/ | 5 | Cross-portal tool-admin surface (page.tsx, CondorToolsClient.tsx, api.ts, logic.ts, types.ts) over the tool catalog. |
app/api/condor/ | 18 routes | BFF Route Handlers proxying ddx-api's condor.controller.ts — chat/[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 routes | catalog, health, probe — the tool-admin BFF. |
components/ddx/condor/ | ~145 | The UI module: CondorProvider.tsx (headless core), cards/ (21), renderers/ (42), shell/ (70, incl. ComputerPanel/ + RunSidePanel/), hooks/ (8), types.ts, variants.ts. |
lib/condor/ | ~48 | Stream + 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.
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:
fetch+ReadableStream, notEventSource. The browserEventSourceAPI cannot send custom request headers and always issues a bare GET. Usingfetchwith a manual SSE line-parser threads anAbortControllerfor clean teardown onrunIdchange/unmount and lets the hook pass its ownLast-Event-IDon reconnect (use-ag-ui-stream.ts:26-33).- 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
visibilitychangeback 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). - Non-retryable statuses stop immediately —
400, 401, 403, 404, 410are 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/-cardetc. and is hand-mapped by an explicitcaseblock tocondor.tool.*. Consumers subscribe to the projected names:condor.tool.started,condor.tool.progress,condor.tool.completed,condor.tool.failed, pluscondor.task.*andcondor.run.*(lib/condor/agui-run-state-fold.ts:111-232, :310-323). For these framesdata-condor-on the wire is correct. - Fallthrough frames — any new, un-cased
data-<suffix>is projected verbatim ascondor.<suffix>; thecondor-segment is not stripped. So a new frame emitted asdata-condor-foodoubles tocondor.condor-foo, and a consumer subscribing tocondor.foorenders 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.
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,MatcherResultCardand 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.tsxrender model-emitted fenced content, plusui-block-spec.ts/ui-block-reconcile.tsfor 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.denied — capability-lifecycle.ts:56-166).
Shell, run inspector, terminal, and workspace
components/ddx/condor/shell/ (70 files) composes the surface:
- Chat chrome —
ChatComposer.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 rail —
SessionsSidebar.tsx,SessionsList.client.tsx,SessionRow.tsx,EditableSessionTitle.tsx,RenameSessionDialog.tsx,NewSessionButton.tsx. - Run inspector —
RunSidePanel.tsx+RunSidePanel/,RunPanel.tsx,RunStatusPill.tsx,RunTimer.tsx,UsageBadge.tsx,TranscriptStream.tsx,ErrorFrameBanner.tsx,BannerStack.client.tsx. - Entity linkage —
CondorEntityLinkBar.tsx,CondorEntityLinkPicker.tsx,entity-link-loaders.ts,entity-link-types.tsbind the session to a patient/visit. - Rehydration —
RehydratePlanSeed.client.tsxandRehydrateRunStateSeed.client.tsxseed 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 workspace —
shell/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.tsx→XtermClient.tsx, themed byterminal-theme.ts).WorkspaceArtifactCard.tsxsurfaces 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-apidirectly — except the PTY WebSocket. Every HTTP round-trip goes throughapp/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 noapi.logentry. The sandbox terminal is the sole carve-out (WS is unproxyable) and is token-gated by the BFF hint. - Styling uses semantic Toucan tokens only —
bg-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 (fromagui-artifacts.ts's totaldefault) is the fastest confirmation that a frame arrived under an unexpected name. - Frontmatter/KB gate —
cd ddx-web && pnpm kb:validate(scripts/kb-validate-frontmatter.ts) must pass before commit.
Related Topics
- 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.