Terminal UI components for agent harnesses.
Chat components exist. Harness chrome does not. agentparts is the second
kind: the status row that tells you the agent is alive and what it is costing
you, the approval prompt that gates a click on your real desktop, the home
screen that shows which model and which screen you are about to point at
something. Those are the parts that make a harness feel like a harness, and
every project currently rebuilds them.
Status: early but complete for v0.1. Eleven components.
| Package | What it is | Runtime |
|---|---|---|
@agentparts/core |
Models and layout arithmetic, no renderer | node and bun |
@agentparts/opentui |
The components, for OpenTUI + React | bun only |
The split is not decoration. A Python prototype of this interface shipped four UI bugs, and all four were the same kind: a border one cell short, a status row one cell too wide, a dropdown reserving eight rows for two matches, a prompt box three rows taller than its contents. None were bugs about what to show.
So core owns everything that can be wrong by one cell, and proves it with
plain unit tests that need no terminal. opentui maps the result to coloured
text and hangs event handlers on it. @opentui/react requires bun; core
deliberately does not, so an Ink adapter or a plain node CLI stays possible.
Not on npm yet. Until the packages are published, install from a checkout:
git clone https://github.com/arthurkatcher/agentparts
cd agentparts
bun installYour app can then live in the workspace and depend on @agentparts/opentui
(and @agentparts/core) with "workspace:*", or copy packages/ into your
own repo; the code is MIT and laid out to be vendored. npm publishing is
planned, and the packages are already shaped for it.
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
import { ThemeProvider, TurnStatus } from "@agentparts/opentui"
function App() {
return (
<ThemeProvider>
<TurnStatus
width={process.stdout.columns}
activity="click(640, 400)"
phase="acting"
phaseStartedAt={startedThisPhase}
turnStartedAt={startedThisTurn}
tokens={12437}
tick={frame}
onStop={() => session.stop()}
/>
</ThemeProvider>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)Renders, at 80 columns:
⠋ click(640, 400) 1m20s 1m20s ⇣12.4k [stop]
Components take props, never events. A TurnStatus is handed an activity
string and two timestamps, not a RunStartEvent, because that is what lets
the same component serve a Python harness, a TypeScript one, and a test.
Adapters from a specific event stream are a separate, optional thing.
Every component reads a Theme from context: a nine-colour palette, a border
style, a glyph set, and a density. Restyling never means forking behavior.
import { ThemeProvider, extendTheme, defaultTheme, monoTheme } from "@agentparts/opentui"
const brand = extendTheme(defaultTheme, { palette: { accent: "#ff6b35" } })
<ThemeProvider theme={brand}>…</ThemeProvider>monoTheme drops every colour and swaps to ascii glyphs. It is not a second
colour scheme: a terminal without colour should look plainly unstyled rather
than like a theme that failed to load. Layout is identical either way, which
is why themeForEnv(process.env) can honour NO_COLOR safely.
| Component | What it is |
|---|---|
HarnessShell |
Four bottom-anchored regions: main, interrupt, status, prompt |
Transcript |
Sticky scrollback over structured entries |
UserMessage |
What the human said, with a coloured gutter |
AssistantMessage |
What the agent said, plus a streaming cursor |
ThinkingBlock |
Reasoning, collapsed by default |
ToolCall |
Marker, call, args, duration, result |
ApprovalPrompt |
"May I do this?", with the safe option first |
TurnStatus |
Cue, activity, both clocks, tokens, stop |
PromptInput |
Bordered input with an in-flow slash-command menu |
HomeScreen |
What you are pointed at, and what you ran last |
ShortcutsBar |
The bottom row, sheddable, with a notice mode |
Four of these do not exist anywhere else: TurnStatus, HomeScreen,
ShortcutsBar and HarnessShell. They are what make a harness feel like a
harness, and they are the reason this is not another chat-component library.
bun install
bun run demoA fake harness with no model, screen or network. Enter runs a scripted
session; partway through, an approval stops everything until you answer.
Ctrl+T toggles the monochrome theme. See examples/demo.
› open a terminal and report the kernel version
▾ thought for 2.1s
• The screen is a bare desktop. The usual shortcut is ctrl+alt+t, so try that
before hunting for a menu.
✓ key(ctrl+alt+t) 0.8s
no change
╭─ allow this action? ───────────────────────────────────────────────────────╮
│ right_click(640, 400) │
│ on x11:managed │
│ │
│ [y] allow run this one action │
│ [n] deny skip it and tell the agent why │
│ [a] always stop asking for this session │
╰────────────────────────────────────────────────────────────────────────────╯
◉ waiting on you 0.5s 4.0s ⇣4.1k [stop]
╭─ demo-model-v0-1 ──────────────────────────────────────────────────────────╮
│ › send a message │
╰────────────────────────────────────────────────────────────────────────────╯
enter send ctrl+c quit / commands ctrl+t theme
bun install
bun run check # build, typecheck, bun tests, node testsIndividually: bun run build, bun run typecheck, bun test,
bun run test:node.
Tests come in three kinds, and each catches something the others cannot:
- Unit tests on
core, no renderer. Where width-exactness is proven. - Render tests via OpenTUI's
testRender, asserting oncaptureCharFrame()and, for colour-only distinctions,captureSpans(). Where "it fits in 40 columns" is proven against an actual frame buffer rather than against arithmetic that agrees with itself. - Snapshots at 40, 80 and 132 columns, committed. CI fails if a build rewrites one, because an unreviewed snapshot records nothing.
The render tests earned their place. Three bugs they caught that unit tests
could not: an approval prompt that read its selection from a stale render
closure, so arrow-then-enter granted permission the user had not given; an
overlay that painted over the transcript without an opaque background,
interleaving into open aPERMISSIONaREQUIREDname -a; and a transcript that
grew downward from the top of the pane, moving the newest line every turn.
v0.1 is done: eleven components and a demo.
Next, in rough order:
- the computer-use components no chat-shaped library will build:
ScreenPane,ActionOverlay,StepCard - an Ink adapter, so node users get the components too.
corestaying runtime-clean is what keeps that door open - shadcn-registry distribution, so presentation can be copied and edited rather than only configured
MIT


