-
Notifications
You must be signed in to change notification settings - Fork 3
Interactive Renderers
The src/interactive/renderers/ directory holds pure functions that transform structured session data into styled terminal rows for the Clio TUI. Each module covers one content kind: tool execution segments, worker run cards, fenced code highlighting, edit diffs, structured JSON/XML output, compaction and branch summaries, provider error presentation, transcript notices, skill surface rows, doctor reports, retry statuses, and reference cards. All renderers return string[] (one array element per terminal row) and carry no I/O, no console writes, and no mutable state beyond the module-level clioTheme() handle.
The primary consumer is the ChatPanel (src/interactive/chat-panel.ts), which composes these renderers into the live transcript. The replay path (src/interactive/chat-renderer.ts) reuses the same renderers so a saved session renders identically to a live one. The DispatchBoard (src/interactive/dispatch-board.ts) also calls presentWorkerContractAnswer for worker contract summaries.
Each file owns one rendering concern:
| File | Key exports | Input | Output |
|---|---|---|---|
tool-execution.ts |
renderToolExecution, renderToolSubline, renderToolPreview, renderToolArguments, renderToolAwaitingApproval, renderToolResultOnly, renderBashTranscriptExecution, hasToolBody, renderFoldedGroup, toolFoldFamily, toolRowTitle
|
ToolExecutionStart / ToolExecutionFinished / BashTranscriptExecution
|
string[] rows |
worker-entry.ts |
renderWorkerEntryLines |
WorkerEntryState |
string[] rows |
worker-answer.ts |
presentWorkerContractAnswer, safeWorkerAnswerText, exactWorkerAnswerObject
|
text, contract, complete, settled
|
PresentedContractAnswer | null |
code-ink.ts |
codeInk |
lang, lines
|
string[] |
diff.ts |
renderDiffLines, parseDiffLine, styleLine, renderIntraLineDiff
|
diffText, width, options
|
string[] |
structured.ts |
tryRenderJson, tryRenderXml
|
value, width, options
|
string[] | null |
compaction-summary.ts |
renderCompactionSummaryEntry, renderCompactionSummaryLine, renderCompactionSummaryHeader, renderEvictionSkipLine
|
CompactionSummaryEntry / line inputs |
string[] / string
|
provider-error.ts |
presentProviderError, providerErrorEvidence, terminalSafePrefix
|
value (error text) |
string |
notice.ts |
renderNoticeRow |
text, mark, width
|
string[] |
skill-rows.ts |
renderSkillSurfaceRow, renderSkillSuggestionRow
|
SkillSurfaceChange / line
|
string[] |
branch-summary.ts |
renderBranchSummaryEntry, renderBranchSummaryHeader
|
BranchSummaryEntry, width
|
string[] |
doctor-report.ts |
renderDoctorReport |
findings, width
|
string[] |
mermaid.ts |
createMermaidMarkdownTransform |
theme |
(markdown, width) => string |
reference-card.ts |
renderReferenceCard |
card, width
|
string[] |
retry-status.ts |
formatRetryStatus, renderRetryStatus
|
status, width, detail, unbounded
|
string / string[]
|
preview.ts |
previewRows, previewBudget
|
rows, limit, width, terminalRows
|
string[] / number
|
The ChatPanel calls renderToolExecution (full render) and renderToolPreview (bounded preview) for tool segments. The flow through renderToolExecution:
-
Header line:
sublinePartsresolves the row viaresolveToolRow(fromsrc/tools/presentation.ts), which classifies the tool by name and admission action class. The header carries the class mark, verb, object, scope, inline scalar args, and resource label. -
Operator grant:
operatorGrantRowsrenders a dimallowed by you · <axis>row when the call was preceded by an operator approval. -
Mutation diff: For
mutateclass tools with a successful result,renderMutationDiffBlockcallsrenderDiffLines(src/interactive/renderers/diff.ts) to parse the diff text into numbered added/removed rows, optionally with intra-line diff highlighting. -
Bash echo: For
executeclass tools with acommandarg,renderBashResultBlockemits a$ <cmd>line with syntax highlighting viahighlightBashCommand, then the unwrapped output. -
Result block:
renderResultBlockcallsunwrapResultEnvelope(which handles the{ content: [...] }envelope from pi-agent-core), thenrenderStructuredOutputRows(which triestryRenderJsonandtryRenderXml) or falls back torenderOutputRows. -
Output footer:
renderOutputFooterstates the offload path and follow-up hint.
The renderToolPreview variant budgets rows via previewBudget (from src/interactive/renderers/preview.ts), which caps the number of visible rows based on the TranscriptDetailPolicy and terminal height. Failed calls use the errorRows budget; bash uses bashRows or operatorBashRows.
Wrapper vs. direct: renderToolResultOnly is a wrapper that delegates to renderToolPreview when opts.unbounded is false and opts.detail is set. renderBashTranscriptExecution constructs a synthetic ToolExecutionFinished from a BashTranscriptExecution and delegates to either renderToolExecution or renderToolPreview depending on the unbounded flag.
The renderWorkerEntryLines function in worker-entry.ts renders a worker run card from WorkerEntryState. When the worker has settled and carries a receipt, it calls presentWorkerContractAnswer (worker-answer.ts) to attempt a structured presentation:
-
Gate: The answer is admitted only when
settledis true,completeis true, andcontract?.conformance === "pass". -
Parse:
exactWorkerAnswerObjectextracts the JSON object from the text (optionally stripping one code fence). -
Validate: For
scout-report, every numeric token in the text must round-trip as a safe integer, preventing loss of precision in line numbers or large IDs. -
Dispatch: A switch on
contract.kindcalls one of five report functions (debuggerReport,verifierReport,researchReport,worldKnowledgeReport,scoutReport). -
Sanitize: Every line passes through
safeWorkerAnswerText, which normalizes C1 control characters to their CSI/OSC equivalents, then redacts secrets and strips C0/Cf characters.
The ChatPanel also calls presentWorkerContractAnswer directly (via dispatch-board.ts) for the Fleet Runs board's contract summary.
codeInk (src/interactive/renderers/code-ink.ts) is wired into the markdown theme as the highlightCode hook. The ChatPanel composes:
const CHAT_MARKDOWN_THEME = markdownTheme(clioTheme(), (code, lang) => codeInk(lang, code.split("\n")));codeInk resolves the fence tag to a LangSpec (TypeScript, JSON, Bash, Python, or Diff). For each line it runs a small scanner (scanLine) that identifies comments, strings, keywords, and numbers. The scanner carries state across lines for constructs that legally span lines: block comments, template literals, and triple-quoted strings. The mapping is closed to extension: comments map to dim, strings to success, keywords to reason, and numbers to info. Every other character stays plain.
New tool class or verb: Add an entry to the tool registry in src/tools/presentation.ts (the resolveToolRow function). The renderers in tool-execution.ts read from row.spec.class, row.spec.verbs, and row.spec.object, so a new class is automatically picked up.
New worker contract kind: Add a case to the switch in presentWorkerContractAnswer (worker-answer.ts) and a corresponding report function that returns PresentedContractAnswer | null. The WorkerPresentedResultContract type in worker-stream.ts must also include the new kind.
New notice mark: Add a row to the MARKS record in notice.ts and import the glyph from GLYPH.
New code-ink language: Add a LangSpec constant and a case to resolveSpec. The spec controls line comments, block comments, templates, triples, quotes, and keywords.
New fold family: Add a case to toolFoldFamily (tool-execution.ts) and a corresponding verb in renderFoldedGroup.
Every renderer is pure: no I/O, no console writes, no module-level mutable state beyond the shared clioTheme() handle. The ChatPanel owns the live loop; the renderers are called with data and return rows. This is asserted in the module docblocks and verified by the contract tests, which construct synthetic entries and assert on the rendered output without a running panel.
The presentWorkerContractAnswer gate in worker-answer.ts enforces that a contract summary is shown only when the receipt's integrity check passes and the contract conformance is "pass". The test tests/contracts/completed-worker-presentation.test.ts verifies that a tampered receipt (modified after sealing), a missing-ledger receipt, and a retired receipt all produce the raw output with a note that the seal is broken or unavailable, never the structured summary. The worker-card-mutation-report.test.ts test verifies that a mutation report that outgrew the live tail settles into prose, not raw JSON.
The codeInk module explicitly states: "The mapping is closed to extension. When the lexer is unsure it leaves text plain: under-highlighting is correct behavior, mis-highlighting is a defect." The INK_TOKEN record maps exactly four InkKind values to theme tokens, and no other path writes code color.
The transcript uses a two-cell gutter plus a content column. Action rows use the ▸ glyph in the gutter; their body nests under the rail ( │). The wrapHanging function in tool-execution.ts ensures that a wrapped action row's continuation starts in the content column (indented two cells), never at column 0. The renderFoldedGroup function for Compact style folds runs the same discipline: the fold header row hangs, and the nested targets use indentAndWrap with the rail prefix.
presentProviderError in provider-error.ts scans the error text for a terminal-safe prefix (up to SCAN_LIMIT characters), then applies a display limit (DISPLAY_LIMIT). The test tests/contracts/provider-error-presentation.test.ts verifies that huge malformed inputs (5 MB of x characters, 100,000 unterminated JSON objects) produce output under 700 characters with the available diagnostic marker. OSC and CSI sequences spanning the scan boundary are neutralized: the test OSC and CSI spanning the scan boundary cannot expose payloads verifies that a 9000-character OSC payload followed by AFTER_CONTROL yields only HTTP 503 AFTER_CONTROL in the primary projection, while the full evidence (via providerErrorEvidence) retains the tail.
- Production panel bounds a provider failure while View and export retain redacted evidence: Constructs a chat panel, applies a 503 error with a 10,000-character payload, and asserts the rendered output is under 1,500 characters, does not contain the fixture secret or the evidence tail, and that the inspection artifact retains the evidence tail but not the secret.
-
Retry countdown replaces its row and preserves a visible final failure: Applies four retry phases (
scheduled,waiting,retrying,exhausted) and asserts one↻mark, no[retry]tag, and the exhausted message. - OSC and CSI spanning the scan boundary: Verifies that five different OSC/CSI payloads (each 9,000 characters) are fully neutralized in both the primary and full evidence projections.
-
43-column primary failures and retries obey style row budgets: For each style (
compact,standard,detailed), asserts that the rendered rows at 43 columns stay within the style's budget (12 for detailed, 4 for others).
-
A mutation report that outgrew the live tail settles into prose: Constructs a ~5 KB mutation report, streams it through a worker stream (which cuts the live tail), then settles it. Asserts that
renderWorkerEntryLinesproduces prose (changed lib/math.js, test/math.test.js) not raw JSON, and that the sealed answer lost no bytes.
- Recognized settled debugger reports share diagnosis and reproduction truth at narrow widths: For widths 43, 80, and 120, asserts that both the transcript and the fleet board show the diagnosis and reproduction status, and that the raw JSON keys are not leaked.
-
Live, failed-contract, partial, malformed and unknown answers never acquire a contract summary: Verifies that a
pendingworker, afailcontract, a truncated worker, a malformed JSON, and an unknown contract kind all produce raw output, never the structured summary. -
Scout escaped and literal keys preserve unsafe numeric tokens: Tests that escaped Unicode keys (
"\u006cine","li\u006ee") and tokens that cannot round-trip as safe integers (1.00000000000000001,9007199254740993,1e400,-0,123456789012345678901234567890) are preserved as source text, not coerced.
-
The island and the card name the same activity: For each progress phase (
starting,thinking,writing,tool), asserts that theformatTaskIslandLinesoutput and therenderWorkerEntryLinesoutput both contain the phase word and neither says· running ·.
-
Worker preview favors current work and preserves pending input: At widths 40, 44, 60, 92, and 120, asserts that
renderWorkerEntryLineskeeps the current action visible and preserves aneeds_inputcheckpoint question.
- Keeps reasoning, prose, and tools in stream order: Asserts that the rendered output respects the event order: reasoning → prose → tool → reasoning → prose.
-
States a skill suggestion as a § row: Asserts that
Suggested skill: /skill tddin the model's prose is rendered as a§row with the command in accent color, not as the agent's prose. -
Changes the preset while a tool remains live: Switches from
detailedtocompactmid-stream and asserts that the reasoning is folded to a marker while the tool command remains visible. -
Marks every operator prompt row with the bar: Asserts that pending prompts show
· preparingand committed prompts are bold.
Do not add module-level mutable state: The purity contract is load-bearing. The ChatPanel and ChatRenderer assume that calling a renderer twice with the same arguments produces identical output. A mutable module variable would break the replay path and the test harness.
Do not post-process codeInk output: The codeInk function is the only module that composes code color. The ChatPanel wires it through the markdown theme's highlightCode hook. Adding a second pass that re-colors the result would double-apply ANSI sequences and break the CHAT_MARKDOWN_THEME invariant.
Do not add a sixth InkKind: The mapping is closed. If a new syntax category is needed, the correct approach is to map it to one of the existing four tokens (e.g., map function names to keyword) rather than adding a new token and color.
Do not change the wrap discipline without updating hasToolBody: The hasToolBody function in tool-execution.ts checks whether any row starts with RAIL_DIM or RAIL_ERROR. The ChatPanel uses it to decide whether to insert a blank line around a tool block. A change to the rail prefix (e.g., from │ to something else) must update hasToolBody in the same change.
Do not weaken the presentWorkerContractAnswer gate: The three-part gate (settled && complete && contract.conformance === "pass") is the only thing preventing a live or partially-sealed worker from acquiring a structured summary. The worker-card-mutation-report.test.ts regression exists because a stale droppedBytes count caused a settled answer to be misclassified as truncated.
Do not change the MODEL_NOTE_LINE regex without updating splitModelNotes: The regex /^\[middleware:[a-z-]+\] (.+)$/u matches the tag format that the middleware domain writes. If the middleware changes its tag format, both the regex and the splitModelNotes function must change together, and the test in rendering-invariants.test.ts (states guidance middleware attached for the model as one note) must pass.
Do not alter BLOCK_REASON_LIMIT or INLINE_ARG_LIMIT without checking the statusGlyph and sublineParts composition: These limits control how much text rides on the status tail and the inline scalar args. The wrapSublineWithTail function assumes the tail is atomic (it wraps the lead and places the tail as a single unit). A longer limit would increase the probability of the tail not fitting beside the lead, causing it to fall to its own row.
Do not remove the terminalSafePrefix scan from providerErrorEvidence: The SCAN_LIMIT constant (200) bounds the scan for the terminal-safe prefix. The providerErrorEvidence function is called by the inspection artifact loader, which must retain the full safe evidence for the /view overlay. Removing the scan would let a 5 MB error text flood the artifact.
Source and generation metadata
title: "Interactive renderers"
summary: "Pure functions that transform session data, tool results, and worker answers into terminal-styled text rows for the Clio TUI."
sources:
- "src/interactive/renderers/tool-execution.ts"
- "src/interactive/renderers/worker-entry.ts"
- "src/interactive/renderers/worker-answer.ts"
- "src/interactive/renderers/code-ink.ts"
- "src/interactive/renderers/diff.ts"
- "src/interactive/renderers/structured.ts"
- "src/interactive/renderers/compaction-summary.ts"
- "src/interactive/renderers/provider-error.ts"
- "src/interactive/renderers/notice.ts"
- "src/interactive/renderers/skill-rows.ts"
- "src/interactive/renderers/branch-summary.ts"
- "src/interactive/renderers/doctor-report.ts"
- "src/interactive/renderers/mermaid.ts"
- "src/interactive/renderers/reference-card.ts"
- "src/interactive/renderers/retry-status.ts"
- "src/interactive/renderers/preview.ts"
symbols:
- "renderToolExecution"
- "renderToolSubline"
- "renderToolPreview"
- "renderToolArguments"
- "renderWorkerEntryLines"
- "presentWorkerContractAnswer"
- "codeInk"
- "renderDiffLines"
- "tryRenderJson"
- "tryRenderXml"
- "renderCompactionSummaryEntry"
- "presentProviderError"
- "renderNoticeRow"
- "renderSkillSurfaceRow"
- "renderBranchSummaryEntry"
- "renderDoctorReport"
- "createMermaidMarkdownTransform"
- "renderReferenceCard"
- "formatRetryStatus"
- "previewRows"
- "previewBudget"
tests:
- "tests/contracts/provider-error-presentation.test.ts"
- "tests/contracts/worker-card-mutation-report.test.ts"
- "tests/contracts/completed-worker-presentation.test.ts"
- "tests/contracts/fleet-island-activity.test.ts"
- "tests/contracts/tui-workbench-ergonomics.test.ts"
- "tests/extended/rendering-invariants.test.ts"
invariants:
- "Every renderer is a pure function: no I/O, no console writes, no module-level mutable state beyond the shared theme handle."
- "codeInk only colors four tokens (comment, string, keyword, number) and never post-processes the result; unrecognized fence tags return plain text."
- "Worker contract answers are admitted only when the receipt's integrity check passes and the contract kind matches one of the five recognized result envelopes."
- "The transcript renders a two-cell gutter plus a content column; action rows hang their continuation in the content column so a long dispatch never wraps to column 0."
validate:
- "pnpm test"Clio Coder · Repository · Website · Documentation
Wiki v0.1 · Developing implementation reference · Source snapshot: 657dce13d. Authored architecture documents define the product contracts.
- Clio Coder GUI Client
- apps / clio-coder-gui
- Apps clio coder gui server
- Apps clio coder gui tests
- apps
- Architecture
- Command-line surfaces
- Core
- Domains agents
- Config Domain
- Context Domain
- Dispatch domain
- Domains evidence
- Domains extensions
- Domains gateway
- domains
- Domains interop
- Domains lifecycle
- Domains memory
- Middleware Domain
- Domains mux
- Domains observability
- Domains plugins
- Prompt Compiler
- Domains providers
- Domains quota
- Domains resources
- Domains safety
- Domains scheduling
- Domains session
- Vendored Tool Registry and Resolution
- Engine
- Engine acp
- Engine apis
- engine
- Entry point
- Interactive
- interactive
- Interactive overlays
- Interactive renderers
- clio-coder wiki
- Scripts
- Contract tests
- Tests extended
- tests
- Tools
- Tools data
- tools
- Tools verify
- Worker runtime