An agent's UI has exactly two settings today: take a whole built-in surface, or build everything yourself. Nothing in between. That canyon is why a Repo Coder — which owns one repo and is driven by its Lead — shipped with add-repo, a multi-repo list, and a duplicate way to drive an engine: declaring surfaces:["coding"] was the only way to get a terminal.
Surface options (ba415d4) patch the symptom. The cause is that surfaces are monolithic.
What an audit of the console found
The blocks are already generic, just not extractable.BoardTab literally declares GENERIC_COLUMNS and is mounted for every agent without a dedicated surface. TerminalView + render-terminal.ts (~110 LOC) have zero hooks, zero endpoints, zero router, zero context — fully controlled by props.
And they are already duplicated. The chat thread exists three times — InstanceDetail.tsx, CopilotView.tsx, AgentDetail.tsx — near line-for-line, including the same tool-call regex, the same voice pill, the same scroll-to-bottom threshold, plus CopyButton and playMessage defined twice each. There are five bespoke relative-time formatters, none using the SDK's formatTime. Two kanban boards. Two file lists. The console renders terminal output as an uncolorized <pre> while the coder colorizes it.
Every one of those is a block that wanted to be shared and had nowhere to live.
Proposed order (by extraction value)
Terminal — trivial, self-contained, ~110 LOC. Immediately upgrades the uncolorized <pre> on the Terminals page. Do this first.ChatThread — biggest payoff: six separate duplications collapse. Needs ~200 lines lifted out of InstanceDetail and a renderMessageBody slot so the gloss layer stays out of the library.RunList/Timeline — three components converge; props are already just {instanceId} and only lookup tables differ.Board — one useNavigate to invert into onOpenItem, plus hoisting the apply-retry and BROWSER_TASK run form into props.FileList + FilePreview — blocked only on relocating UploadJob.- Primitives that fall out:
Modal, CopyButton, EmptyState, StatusPill — and delete the five time formatters.
There is a precedent to copy: agents/coder/web is already a workspace package with React as a peer dependency.
Blockers to settle first
- Theme tokens live only in the console (
@theme in index.css). Every candidate block is saturated with bg-panel border-line text-accent. A package built outside the console emits classes with no definitions. - Global CSS the components silently need:
.msg-md, .chat-scroll, scrollbar-none. - The Tailwind cross-package scan is a one-off hack — a literal
@source "../../../agents/coder/web/src". Every new package needs another line, and no @source can ever cover a third-party bundle built outside this repo. alert()/confirm() inside candidate components must become injected confirm/toast.
The decision that shapes everything
See #187. Whether third-party surfaces consume React components or a DOM-mount API determines the library's entire public shape, so decide it before writing component code.
An agent's UI has exactly two settings today: take a whole built-in surface, or build everything yourself. Nothing in between. That canyon is why a Repo Coder — which owns one repo and is driven by its Lead — shipped with add-repo, a multi-repo list, and a duplicate way to drive an engine: declaring
surfaces:["coding"]was the only way to get a terminal.Surface options (
ba415d4) patch the symptom. The cause is that surfaces are monolithic.What an audit of the console found
The blocks are already generic, just not extractable.
BoardTabliterally declaresGENERIC_COLUMNSand is mounted for every agent without a dedicated surface.TerminalView+render-terminal.ts(~110 LOC) have zero hooks, zero endpoints, zero router, zero context — fully controlled by props.And they are already duplicated. The chat thread exists three times —
InstanceDetail.tsx,CopilotView.tsx,AgentDetail.tsx— near line-for-line, including the same tool-call regex, the same voice pill, the same scroll-to-bottom threshold, plusCopyButtonandplayMessagedefined twice each. There are five bespoke relative-time formatters, none using the SDK'sformatTime. Two kanban boards. Two file lists. The console renders terminal output as an uncolorized<pre>while the coder colorizes it.Every one of those is a block that wanted to be shared and had nowhere to live.
Proposed order (by extraction value)
Terminal— trivial, self-contained, ~110 LOC. Immediately upgrades the uncolorized<pre>on the Terminals page. Do this first.ChatThread— biggest payoff: six separate duplications collapse. Needs ~200 lines lifted out ofInstanceDetailand arenderMessageBodyslot so the gloss layer stays out of the library.RunList/Timeline— three components converge; props are already just{instanceId}and only lookup tables differ.Board— oneuseNavigateto invert intoonOpenItem, plus hoisting the apply-retry and BROWSER_TASK run form into props.FileList+FilePreview— blocked only on relocatingUploadJob.Modal,CopyButton,EmptyState,StatusPill— and delete the five time formatters.There is a precedent to copy:
agents/coder/webis already a workspace package with React as a peer dependency.Blockers to settle first
@themeinindex.css). Every candidate block is saturated withbg-panel border-line text-accent. A package built outside the console emits classes with no definitions..msg-md,.chat-scroll,scrollbar-none.@source "../../../agents/coder/web/src". Every new package needs another line, and no@sourcecan ever cover a third-party bundle built outside this repo.alert()/confirm()inside candidate components must become injected confirm/toast.The decision that shapes everything
See #187. Whether third-party surfaces consume React components or a DOM-mount API determines the library's entire public shape, so decide it before writing component code.