assistant output — tool commands leak as plain text, durations are lost, and errors are invisible.
The Problem
Most agent CLIs emit structured stdout with tool calls, progress indicators, and multi-line output. For example:assistant text — the tool calls and results are indistinguishable from the agent’s actual response.
With a parser, the UI renders:
Thinking about how to approach this...as a collapsible thinking block$ ls /home/user/projectas a tool call card (collapsed)0.3sduration as a tool result cardThe project is a CLI tool...as the assistant’s response
How It Works
- Build time — You compile
src/ui-parser.tstodist/ui-parser.js(zero runtime imports) - Server startup — Plugin loader reads the file and caches it in memory
- UI load — When the user opens a run, the UI fetches the parser from
GET /api/:type/ui-parser.js - Runtime — The fetched module is eval’d and registered. All subsequent lines use the real parser
Contract: package.json
1. paperclip.adapterUiParser — contract version
2. exports["./ui-parser"] — file path
Contract: Module Exports
Yourdist/ui-parser.js must export at least one of:
parseStdoutLine(line: string, ts: string): TranscriptEntry[]
Static parser. Called for each line of adapter stdout.
createStdoutParser(): { parseLine(line, ts): TranscriptEntry[]; reset(): void }
Stateful parser factory. Preferred if your parser needs to track multi-line continuation, command nesting, or other cross-call state.
createStdoutParser takes priority.
Contract: TranscriptEntry
Each entry must match one of these discriminated union shapes:Linking tool calls to results
UsetoolUseId to pair tool_call and tool_result entries. The UI renders them as collapsible cards.
Error handling
SetisError: true on tool results to show a red indicator:
Constraints
-
Zero runtime imports. Your file is loaded via
URL.createObjectURL+ dynamicimport()in the browser. Noimport, norequire, no top-levelawait. - No DOM / Node.js APIs. Runs in a browser sandbox. Use only vanilla JS (ES2020+).
-
No side effects. Module-level code must not modify globals, access
window, or perform I/O. Only declare and export functions. -
Deterministic. Given the same
(line, ts)input, the same output must be produced. This matters for log replay. -
Error-tolerant. Never throw. Return
[{ kind: "stdout", ts, text: line }]for any line you can’t parse, rather than crashing the transcript. - File size. Keep under 50 KB. This is served per-request and eval’d in the browser.
Lifecycle
Error Behavior
Building
tsconfig.json can handle this automatically — just make sure ui-parser.ts is included in the build and outputs to dist/ui-parser.js.
Testing
Test your parser locally by running it against sample stdout:npx tsx test-parser.ts
Skipping the UI Parser
If your adapter’s stdout is simple (no tool markers, no special formatting), you can skip the UI parser entirely. The genericprocess parser will handle it — every non-system line becomes assistant output. This is fine for:
- Agents that output plain text responses
- Custom scripts that just print results
- Simple CLIs without structured output
exports["./ui-parser"] in your package.json.
Next Steps
- External Adapters — full guide to building adapter packages
- Creating an Adapter — adapter internals and built-in integration