Blueprint: ACP Chat Workspace — reusable components, layout, and beam re-engineering
This document covers two things that outlive any one UI: the reusable component library extracted from beam, and the chat workspace layout those components compose into. It also specifies how beam’s chat is re-engineered onto ACP so it stops being special.
It supersedes and folds in the prior workspace blueprints (sovereign-workspace,
sovereign-workspace-architecture, chat-canvas). Their durable ideas — the
brain/glass split, UX sovereignty, the renderer-agnostic canvas, the
two-artifact model, the terminology discipline, and the truth-gate — are carried
below. What is dropped is deliberate: those documents planned the workspace on
beam’s private endpoints (internalchatapi mode/context, task-event SSE,
WidgetHint plumbing) and positioned contenox serve as the hero surface. This
blueprint routes the same workspace through ACP instead, which is what makes
the capability shared rather than beam-only.
The load-bearing principle
Beam’s chat must be a client of acpsvc — the same agent Zed and any other
ACP client talk to — not a consumer of private runtime endpoints.
The failure mode this prevents: adding one more beam-only endpoint is always
cheaper than landing a capability in the protocol, so every chat feature drifts
into a side door (internalchatapi, task-event SSE, approvals inferred
heuristically from later events). The result is a UI that out-delivers the
runtime’s own protocol surface — at which point the capability has escaped the
product and Zed/API/CLI users don’t have it.
Rule: anything the chat UI can do that a conformant ACP client cannot is a
defect in the runtime’s ACP surface, and the repair direction is always
downward — into acpsvc (as standard methods or sanctioned extension points),
never sideways into a beam endpoint. If the UI ever needs to know it is talking
to contenox specifically to render something, the agent is wrong.
Corollary: the same components and the same client core serve three consumers without divergence — beam’s embedded chat, a future standalone ACP client, and live demos on the website. None of them is privileged; contenox is one agent in a list.
This “ACP client” is the browser/beam side driving contenox upward — not to be
confused with the Go runtime’s own downward ACP client capability (contenox
driving other agents, including other contenox instances), which lives in
the runtime rather than in beam or any web client; see
../acp/acp-client-engine.md. The “future standalone ACP client” above is
scoped to this chat surface; it is not the shape the operator/ops console
takes — beam is the settled screen for that, see
../opsclient/operator-console.md.
Part A — The reusable component library
A.1 Three layers of unequal value
@contenox/ui is not one asset. Audited by intent, it splits three ways:
| Layer | Disposition | Why |
|---|---|---|
| Chat / terminal / diff / tool-call + tokens (~3.2k LOC + token sheet + Storybook) | Keep and extract | No ecosystem equivalent; correct aria-live streaming, i18n-clean label props, dual-theme rigor enforced by designTokenGuard |
| Generic overlays (Dialog, Dropdown, Toast, Tooltip, …) | Keep the API, back with headless primitives | Hand-rolled versions lack focus traps / keyboard nav / portals; permission dialogs demand these. Style the ecosystem primitive with the same tokens |
Product-specific (visualization/, taskTypes.ts mirroring Go schemas, admin scaffolding) | Excise from the library | Product code in the foundation layer; belongs in beam, not in a design system a second product imports |
The keep-layer is the only part with genuine, unique, competitive value. It is also already proven in a second consumer (the VS Code webview). Everything else is either replaceable by ecosystem defaults or is beam-specific.
A.2 The extraction: @contenox/chat-kit
Extract the keep-layer into a presentation-only, transport-agnostic package. Hard rules for what may live in it:
- No data fetching, no protocol knowledge, no contenox types. Components
take plain props and render. A component that imports a
TaskEventshape, a chain type, or an API client does not belong here. - Semantic tokens are the styling contract. The token CSS +
fonts.cssship with the package;designTokenGuardmoves into this package’s test suite (today it lives in beam’s tests, auditing ui from outside — wrong direction). - Every string is an overridable label prop defaulting to English. The
package carries no i18n framework; the consumer supplies translations by
passing props. This is the pattern
ChatMessagealready models. - Headless where interaction is hard. Overlays (dialog, menu, listbox, tooltip, toast) are composed over a headless primitive library and skinned with tokens. Do not hand-roll focus management again.
Component inventory (the seam an ACP client renders against):
| Component | Renders | ACP source it maps from (consumer wires this, not the component) |
|---|---|---|
Transcript / Message | streamed message list, thinking blocks, copy, collapse | agent_message_chunk, agent_thought_chunk, user_message_chunk, grouped by messageId |
StreamingCaret / TypingIndicator | live-generation affordance | chunk arrival between turn start and stopReason |
ToolCallCard | tool title/status/args/output, expandable | tool_call + tool_call_update (status, rawInput, rawOutput) |
DiffView (real LCS line diff) | file diffs inside tool cards | tool_call content of type diff |
TerminalOutput / TerminalEmbed | ANSI output, live terminal attach | tool_call content of type terminal |
PlanPanel | ordered plan entries with status | plan updates |
PermissionDialog | option list, keyboard-first accept/deny | session/request_permission (options with kind) |
ConfigOptionControls | select/boolean dropdowns | configOptions in session responses + config_option_update |
SlashCommandMenu / Composer | input, / and @ completion, soft limit | available_commands_update; composer emits plain prompt text |
UsageMeter | context budget indicator | usage_update (used/size) |
SessionList | pick/rename/delete/resume | session/list + session_info_update |
Named gems to preserve as-is: TerminalPromptInput (no ecosystem equivalent),
the xterm lifecycle wrapper (XTerminal — Strict-Mode-safe deferred connect,
ResizeObserver fit, theme bridge), the slash-command registry (React-decoupled,
/ vs @ split, arg completions), and the token-sheet + designTokenGuard
pairing.
A.3 The client core: @contenox/acp-web-client
A second, separate package: the transport-agnostic ACP client that turns a
connection into the props chat-kit renders. This is the implementation of the
long-dormant BeamChatClient-style seam.
- Depends on the official ACP TypeScript SDK for wire types only, as a
compile-time (
import type) dependency — not for framing or dispatch. The SDK’s own connection layer queues every write asynchronously (no synchronous send) and validates built-in methods against a schema stricter than libacp’s actual traffic, so the message loop stays a thin, fully-owned engine matching libacp’s real contract; it moves onto the SDK’s connection only once the SDK exports a low-level connection, its validation tolerates libacp’s traffic, or the remote-transport RFD stabilizes and its WebSocket transport graduates out of experimental. This is not license for a second hand-rolled protocol implementation alongside it — the runtime already maintains libacp, this client, and the vscode agent’s bespoke RPC; a fourth would drift the same way. - Exposes a small interface:
listSessions / newSession / loadSession / resumeSession / deleteSession / prompt(handlers) / cancel / setConfigOption / respondPermission.promptstreams via handlers (onMessageChunk,onThoughtChunk,onToolCall,onPlan,onPermissionRequest → Promise<optionId>,onUsage). - Connection is a pluggable adapter, so the same core runs over: a
WebSocket to
/acp(browser / demo / remote), or a spawned stdio subprocess (a desktop shell, later). The renderer never knows which. - Knows nothing contenox-specific. Chains arrive as slash commands, model/think/
policy as config options, history as
session/loadreplay — all standard.
Dependency arrow: beam → chat-kit, beam → acp-web-client → chat-kit-types.
chat-kit never imports the client; the client never imports React views.
A.4 Package hygiene carried into the extraction
The current package has debt that must not survive the move: a junk tsc
runtime dep, a dead tailwind-config export, a stale compiled components.css,
and runtime/dev/peer deps that were mis-sorted. Extraction is the moment to fix
these, not replicate them.
Part B — Experience: how it makes people productive
B.0 The productivity thesis
The product is marketed as “AI workflows you can run, review, and own” — a local-first workflow runtime for specific, reviewable AI work, explicitly not “just a chatbot.” The hero demo is the human-in-the-loop approval gate: the agent asks to run a command, waits at the gate, and reports back. The stated value is that the chain is the contract — prompts, tool allowlists, command policy, budgets, and approval gates are visible, diffable, repeatable — and that everything is local, no account, no telemetry.
That framing dictates the look and feel. Users do not arrive wanting a nicer message list; they arrive wanting to trust, review, and own what an agent does. So the workspace optimizes three moments, in priority order:
- The approval moment (trust). A permission request is the product’s signature interaction, not a modal afterthought. It must show exactly what will happen — the command, the diff, the tool and its arguments, the policy that gated it — with a keyboard-first accept/deny and an always-visible “what rule caused this?” affordance. This is where the “review” promise is kept or broken.
- The review moment (comprehension). After (or during) a turn, the user inspects what happened: diffs as diffs, a run as a timeline, a plan as ordered steps, terminal output as a terminal. Reviewing must be as fast as reading — selection-driven, one click from a turn to its artifact, no hunting.
- The ownership moment (control). The chain, policy, model routing, and providers that govern the session are visible and editable from the same place you run them — not hidden in a separate admin app. “The chain is the contract” is only true if the contract is one click away and diffable.
Everything below serves those three moments. A design choice that makes the message list prettier but the approval gate weaker is the wrong choice.
Underneath the marketing thesis is a strategic one: own the governance surface, not the text buffer. Editors (VS Code, Zed) are “open and extensible” until you want AI features, at which point the vendor’s agent chrome owns the context window, onboarding, and review flow. Contenox does not need a text buffer as its product — it needs ownership of what the agent is about to do, what it did, and the policy that governs it. That is the governance surface, and it is exactly the three moments above. Ceding it to an editor’s Copilot pane is the failure this workspace exists to prevent — while still speaking ACP to those editors as a secondary surface, so an operator who prefers to type in Zed keeps the same runtime, policy, and review semantics.
B.0.1 The keep/reject filter
Every proposed workspace feature answers three questions; a “no” routes it elsewhere rather than into the workspace:
- Can the runtime drive this headlessly (ACP / CLI / MCP), without an editor vendor’s UI? If no → fix the ACP surface first (repair downward, §the load-bearing principle). The UI never becomes the only way to reach a capability.
- Does it need the full context window — the run, the artifact, and the verification together? If no → it belongs in the CLI or an editor attachment, not a bespoke panel here.
- Is the primary user action review / direct / steer, rather than character-by-character typing? If no → it is editor work; attach an editor over ACP, do not rebuild one.
B.0.2 Reclaimed and rebuildable assets
Beam’s navigation routes only to the setup wizard, the admin control plane, and the ACP chat workspace; several pieces of built work fell outside that routing and are not wired into any reachable page. The workspace does not start from zero for the pieces that survived — it reclaims those by re-pointing their data source to ACP; the pieces that were deleted alongside the surface that hosted them go back to being new build, scoped smaller by having a prior implementation to learn from:
| Asset | Current state | Role in the workspace |
|---|---|---|
Monaco integration (ChainJsonEditor, PolicyEditor, monacoAppTheme.ts) | live but orphaned, inside unrouted admin pages (ChainsPage, HitlPoliciesPage); ExpandedMessageEditor was deleted with the legacy chat composer and would need to be rebuilt | Code/diff viewer in the review pane; chain/policy editor in the ownership surface; expanded composer |
Canvas/artifact system (WorkspaceSplitPanel, CanvasPanel, useCanvas, TimelinePanel) | deleted along with the legacy chat page they were wired into; lib/artifacts/canvas.ts’s artifact-shape types remain, unused | The review pane itself, rebuilt against session/update: click a turn/event → project to a canvas artifact |
Graph/workflow visualizer (ChainVisualizer, dagre) | live but orphaned, inside the unrouted ChainsPage | Visualize the chain-as-contract in the ownership surface and as a run’s shape |
FileTree file explorer (@contenox/ui) | built, never imported anywhere | Browse the session cwd / additional directories; anchor a file into a diff or the context artifact |
Cmdbar / CommandPanel command palette (@contenox/ui) | built, never imported anywhere | The keyboard spine (§B.5): sessions, slash commands, config options, “jump to” — one surface |
HITL policy editor (PolicyEditor) | live but orphaned, inside the unrouted HitlPoliciesPage | The “what rule caused this?” drill-down from an approval gate |
Rule: prefer reclaiming a built asset (re-pointing its data source to ACP) over rebuilding, wherever the asset still exists. The excise-from-library rule (§A.1) still applies to the packaging of the visualizer and Go-schema mirrors, but the capability is kept.
B.1 What each prior chat surface contributed
Beam built the same chat three times before settling on the single ACP-native surface; each of the three contributed one idea the others lacked, and the workspace design is the union of those ideas, not any single surface:
- The console contributed the turn model (user command → work log → result), durable per-turn hydration, in-session run retention/scrollback, unified diff and approval verdicts inline, keyboard-first approvals, and slash/bang commands — the rendering shape closest to ACP’s own structure.
- The legacy chat page contributed live streamed tokens with caret and a thinking box, the workspace right rail (Timeline / Canvas / Terminal), the blocking-setup guard, not-found/error states, and composer amenities (expanded editor, attachment pills, per-message copy).
ChatSurface’s injected-client contract contributed the pattern this whole workspace hangs on — a surface that receives a client interface, never a transport — plus the ACP-shaped permission model (option arrays withkind).
The console and the legacy chat page are removed outright, superseded by the
single ACP-native surface. ChatSurface’s injected-client contract lives on
in the client core described below; its own view layer is not revived as
part of this workspace (it remains live only as the VS Code webview’s chat,
a separate consumer with its own integration contract).
B.2 The two-artifact model (load-bearing)
Keep the split cleanly, because collapsing it is the classic mistake:
- Context artifact — per-turn, model-visible; what the user attaches to this
message. “I attached this file / terminal output to my prompt.” In ACP terms
it becomes prompt content blocks (text + resource links) plus, where
sanctioned,
_meta. Lives in the composer, sent at turn time. - Canvas artifact — session-scoped, human-visible; the thing being worked on
or reviewed right now. “This is the diff / plan / run we are looking at.”
Driven by
session/updatenotifications, never by a private task-event reducer.
They are different objects with different lifetimes; some events produce both (a preview URL is a canvas artifact; its screenshot becomes a context artifact only when the user explicitly attaches it). Do not overload one for the other.
The review pane is a renderer-agnostic slot, not a fixed viewer. It starts as
an honest empty state (“No artifact selected — diffs, plans, run views, terminal
output, and files appear here.”) and gains renderers incrementally. Each renderer
is a projection of what the protocol already sends — projectUpdateToCanvas(update) → CanvasArtifact | null — so nothing here needs a bespoke backend field:
| Canvas renderer | Projected from | Reclaimed component |
|---|---|---|
diff | tool_call diff content | Monaco read-only diff |
run / timeline | the turn’s session/update stream | TimelinePanel |
plan | plan updates | plan list |
terminal_output | tool_call terminal content | TerminalOutput / XTerminal |
file_excerpt | a file opened from FileTree or a resource link | Monaco read-only |
markdown / message | assistant/step text a user pins | markdown renderer |
Renderer priority follows the productivity thesis: diff and run first (the review moment), then plan and terminal, then file and markdown. Media/preview renderers are last — the canvas is a review surface, not a general document app. State (open/closed, width, selection) persists per session in local storage.
Selection is the whole interaction: the timeline/transcript emits a selected id,
a small pure reducer (useCanvas) projects it to the current artifact, the pane
renders it. The pane never owns run state and never derives domain objects with
useMemo in the page tree.
B.3 Layout — three zones plus a command spine, desktop-first
┌──────────┬───────────────────────────────┬─────────────────────┐
│ Sessions │ Transcript │ Review / Workspace │
│ rail │ (turns: cmd → work → result) │ (Diff / Timeline / │
│ │ │ Terminal / Files) │
│ list + │ live tokens, thinking box │ │
│ resume/ │ tool cards advance in place │ selection-driven: │
│ delete │ plan entries inline │ click a turn/event │
│ + fresh- │ ─────────────────────────── │ → project to one │
│ ness │ Composer: / and @ complete │ canvas artifact │
└──────────┴───────────────────────────────┴─────────────────────┘
╔═══ Permission gate (the signature moment) ═══╗
║ what will run (cmd / diff / tool+args) ║
║ which policy gated it → drill to PolicyEditor ║
║ [Allow once] [Allow always] [Reject] — keys ║
╚═══════════════════════════════════════════════╝
⌘K command palette: sessions · commands · config · jump-to
Zone rules, ordered by the productivity moments they serve:
- The permission gate is a first-class surface, not a rail. It renders the
full request: the command or the diff or the tool + arguments, plus the
policy name that gated it as a link that drills into the (reclaimed)
PolicyEditor— the literal “the chain is the contract, reviewable” promise. Keyboard-first: the option list fromsession/request_permissionmaps to accessible buttons withkind-aware default framing (allow-once / always / reject), Enter/Escape bound, focus trapped, focus restored. On a cancelled turn it tears down cleanly (the runtime now emits$/cancel_request). - Transcript is the spine and renders live. Turn-structured (console’s
model), but each turn renders as it happens — streamed
agent_message_chunks with caret, a thinking box, tool cards that advance throughtool_call_updatein place (never a new card per update), plan entries inline. Closes the single largest current gap (§C.0). - Review pane is selection-driven, never a duplicate stream. Clicking a turn
or an event projects it into exactly one artifact — a diff in Monaco’s
read-only viewer, a run as
TimelinePanel, a plan, terminal output, or a file viaFileTree. Tabs: Diff / Timeline / Terminal / Files. This is the reclaimed canvas system, re-pointed from task-events tosession/update. Reviewing must feel like reading: one click, no hunt, diffs shown as diffs. - Sessions rail is
session/listwith resume/delete andsession_info_updatefreshness; collapses to a drawer on narrow viewports. - Command palette (⌘K) is the keyboard spine — see §B.5.
- Ownership is one click from running. The chain/policy/model/providers
governing the session are reachable from the workspace, not a separate app:
the chain-as-contract opens in
ChainJsonEditor+ChainVisualizer, the policy inPolicyEditor. Editing is diffable; local-first, no round-trip to a service. - The permission gate is phone-usable. Layout is desktop-first, but the
gate is not desktop-only: on narrow viewports the workspace collapses to
transcript + gate, with the option list from
session/request_permissionrendered as thumb-reachable accept/deny controls. This is what makes beam viable as the operator console for remote-administration sessions, not just coding sessions — see../opsclient/operator-console.md.
B.4 Interaction & visual language (what makes it feel productive)
- Keyboard-first, mouse-optional. Every core action — send, stop, approve, deny, switch session, open a config option, jump to a turn — has a key binding, surfaced through the command palette and inline hints. The signature approval interaction is operable without leaving the keyboard.
- Progressive disclosure. Default view is calm: turns, results, the gate. Detail (raw tool args, full run timeline, thinking, the governing chain) is one deliberate expand away — never a wall of JSON by default, never hidden so deep it can’t be reviewed.
- State legibility over decoration. Status is carried by the semantic token
ramps (pending / in-progress / completed / failed, allow / deny), consistent
across tool cards, plan entries, and the gate — the same vocabulary everywhere,
enforced by
designTokenGuard. No bespoke per-component color. - Motion is functional, not ornamental. The streaming caret, a card
advancing its status, an artifact sliding into the review pane on selection —
motion signals what changed. Honor
prefers-reduced-motion. - Trust signals are explicit. “Local, no telemetry” is a product claim the UI should make legible: session/config/state visibly on-device, the governing policy always nameable from the gate, the model/provider for a turn visible in the usage strip.
- Density is deliberate. This is a review/ownership tool for a technical operator, not a consumer chat toy — favor information density (compact turns, monospace where it aids scanning) over whitespace-heavy chat styling, while keeping the calm-by-default disclosure.
B.5 The command palette — the keyboard spine
Reclaim Cmdbar/CommandPanel (built, never wired) as the single ⌘K surface
that unifies what today is scattered across toolbar, sidebar, and composer:
- Sessions: switch / new / resume / delete.
- Slash commands: the
available_commands_updateset, with the same registry that powers/completion in the composer — one source, two surfaces. - Config options: the
configOptionslist (model, think, policy, token budget) as searchable actions instead of a toolbar of dropdowns. - Jump-to: any turn, any tool call, any open artifact.
This is also the accessibility and discoverability backbone: a new user finds every capability by typing, an operator drives the whole workspace without the mouse. It is the one component whose absence most visibly separates “a chat page” from “a workspace.”
B.6 Polish gaps to close (present in none of beam’s three prior chat implementations)
- Syntax highlighting in transcript code blocks and diffs — once the review pane reclaims Monaco (§B.0.2) that renderer gets it for free; the inline transcript still needs a lightweight highlighter of its own.
- Virtualization of long transcripts / large tool outputs.
- Consistent i18n — several keep-layer components still carry hardcoded English aria-labels and status strings; these become label props during extraction.
- Empty / error / not-found / broken-setup states as designed surfaces, not blank panes — the blocking-setup guard the legacy page had, generalized.
Part C — Re-engineering beam (incremental, truth-gated)
Non-negotiable: no big-bang rewrite. Each slice ships behind the existing product-surface-truth gate — it must be demonstrably real (a reproducible end-to-end check) and must not break plain chat, terminal, approvals, or the VS Code webview. Slices are ordered so the runtime surface is repaired before the UI is asked to consume it.
C.0 The protocol ceiling (downward, in acpsvc)
The UI renders only what the protocol emits, and the workspace consumes only ACP — no chat capability may be built by reaching around this ceiling with a private endpoint. Three points define where the ceiling currently sits, each wire-verified against a conformant client:
- Live token streaming over ACP — DEFERRED BY DESIGN, not a repair. Contenox
was designed for managed agents running in the background with no human
watching, where a chain is a sequence of steps that act as guardrails: the
next step (validator, router, policy gate, HITL check) evaluates the complete
output of the previous one before anything proceeds. Streaming tokens to a
human mid-step would surface output that has not yet passed its guardrail —
which is antithetical to “reviewable, gated, repeatable work.” So the blocking
whole-message path is the architecturally honest default, not a bug: it is what
makes a step’s output a reviewable unit. Do not “fix” it thinking it is an
oversight.
Mechanically: the streaming branch (
taskexec.goexecuteLLM) is gated onlen(tools) == 0; ACP answer tasks carry"tools": ["*"], so they take the blocking path and emit oneagent_message_chunkat end-of-turn. Streaming works and is per-token at the provider; the gate (not the provider) is what turns it off. The gate is provider-agnostic — ALL providers are affected — andStreamParcelalready carries aToolCallsfield for the eventual fix, but local llama’sStream()returnsUnsupportedFeatureErrorfor tool calls, so even opening the gate is real per-provider work. Interactive chat and the guardrail model are in genuine tension, not a missing feature. Build streaming only when interactive UX actually demands it, and when it does, the philosophically-aligned answer is chain-shaped, not core-shaped: stream ONLY the final, user-facing answer step — the one step with no downstream guardrail to feed — via a dedicated streaming answer task, while every intermediate (guardrail-feeding) step stays blocking. That avoids the high-blast-radius change to coreexecuteLLMentirely. Decoupled from C.1–C.4: the UI renders the final answer without it, the same way beam’s prior console-based surface did. /acptransport oncontenox serveis the only chat path in or out ofacpsvc. A WebSocket endpoint wraps the connection as anio.ReadWriteCloserand hands it tolibacp.NewAgentSideConnection(conn, acpsvc.New(deps)), alongside serve’s other WebSocket path (terminal sessions), inheriting serve’s bearer-token auth (mandatory off loopback). Every consumer — this workspace, a future standalone client, a demo — reachesacpsvcthrough this one endpoint; there is no other route.- Replay fidelity remains a gap:
session/loadreplays messages and tool calls but not full execution-event fidelity (step granularity, retries, timings), a level of detail beam’s prior console-based surface had from its own journal hydration. Richer replay benefits every client and closes that gap without a private endpoint compensating for it.
C.1 Extract the library
Split chat-kit and acp-web-client out of @contenox/ui/beam per Part A.
Beam keeps importing them; the VS Code webview repoints to the new packages
(removing the current relative path-reaching). Excise visualization/ and the
Go-schema mirrors from the library into beam. Move designTokenGuard into
chat-kit.
Truth gate: @contenox/ui consumers (beam, webview) build and render
unchanged; the token guard runs green inside the new package.
C.2 Back the overlays with headless primitives
Replace the internals of Dialog / Dropdown / Toast / Tooltip with a headless
library, keeping the public props and token styling. The PermissionDialog
built on this is the acceptance case.
Truth gate: a permission dialog traps focus, is keyboard-operable end to end, and restores focus on close; existing overlay consumers are visually unchanged.
C.3 The adapter: beam chat speaks only ACP
The workspace’s data layer runs entirely against the /acp WebSocket:
session/update handlers replace task-event reducers, session/ request_permission replaces heuristic approval inference, configOptions
replace hand-plumbed toolbar state. The turn/retention/hydration model that
made beam’s prior console-based surface usable ports behind the client
interface — the rendering ideas survive extraction (§B.1); the private data
path they used to run on does not. Once acp-web-client is extracted (C.1),
this adapter is what it becomes; until then the same rule holds for the
client code living directly in beam.
Truth gate: beam chat, driven entirely over /acp, reaches parity with beam’s
prior console-based surface on a real turn (≥3 distinct update kinds, live
tokens, a tool card, a plan, a permission round-trip) — and the identical turn
works from Zed and the headless CLI, proving no beam-only path remains.
C.4 No side door outlives its last consumer
The legacy chat page and the private chat endpoints (the internalchatapi
chat path, the bespoke approval inference) exist only as long as something
still calls them. Once the adapter (C.3) covers a capability, the page or
endpoint that used to provide it is removed in the same arc — not kept as a
fallback, not left for a later cleanup pass. Beam’s scope is the setup
wizard, the admin control plane, and the ACP chat workspace; nothing else
serves chat.
Truth gate: no chat code path reaches the runtime except through /acp; a grep
for the removed endpoints returns only history.
C.5 Standalone client and demo (optional, same core)
With C.1–C.3 done, a standalone client is the same chat-kit + acp-web-client
in a different shell (web build, or Electron/desktop with a stdio adapter), and a
website demo is the same packages pointed at a hosted, hardened (acpx profile,
rate-limited) contenox over /acp. No new UI, no new protocol surface.
Scope note: this is a standalone chat client — a different shell for the same
components. It is not a standalone ops/administration app; that surface’s
answer is beam itself, served from the operated box (see
../opsclient/operator-console.md), not a second client product.
Non-goals
- Fork or embed an editor core. No custom text buffer, no competing on typing latency. Editors attach over ACP; they are not rebuilt here.
- File-tree-first landing. The workspace opens on the run and the gate, not a
filesystem.
FileTreeis a review affordance, not the home screen. - Rebuild orchestration in the client. Chains, HITL, sessions, taskengine stay in the Go runtime — the single source of truth. The client renders and steers; it never executes or owns durable state.
- Monaco as the primary surface. Monaco appears only inside a canvas renderer (diff / file / chain-or-policy editor), never as the workspace’s main buffer.
- Multiplayer / real-time collaboration in the first cut. Review sharing is a later link-based feature, not co-editing.
Terminology discipline
Use workspace, transcript, canvas, timeline, verification, gate. Avoid IDE / ADE / “replacement for VS Code” in product copy, marketing, and code comments — beam is the governance control center, not a typist’s desk. Do not call the run log or timeline the “primary pane”; the transcript and the gate are primary, the canvas is the review surface.
Invariants (anti-patterns to reject in review)
- A chat feature that lands as a beam endpoint instead of an
acpsvccapability. (Repair downward.) - A
chat-kitcomponent importing a contenox type, an API client, or aTaskEvent/chain shape. (Presentation only.) - A hand-rolled focus trap / keyboard menu / portal. (Use the headless primitive.)
- A hardcoded user-facing string without an overriding label prop.
- The UI holding a capability no conformant ACP client can obtain. (The advantage has escaped the product.)
- A new hand-rolled ACP protocol implementation alongside the existing one. (One dispatch engine; the official SDK is a compile-time type source until it exports a low-level connection, tolerates libacp’s real traffic, or its WebSocket transport graduates out of experimental — see §A.3.)
- Deriving core domain objects with
useMemoin the page render tree instead of a pure reducer oversession/update. - A big layout rewrite before the panels have content, or deleting the terminal / legacy path before its replacement is demonstrably better.