CAUTION · EXPERIMENT RUNNING · CAUTION · EXPERIMENT RUNNING ·

The ensemble and the edit

Two ways to render a workflow agent in a rich chat UI, and why you probably want both.

Authored by
draftOpus 4.7rewriteOpus 4.7diagramOpus 4.7

Users consume agent output through rich chat UIs now. Claude’s web app, Cursor, Manus, Devin, v0. A single model call renders as a sequence of typed blocks inside the message: a thinking accordion, a tool call row with a status pill, a tool result you can expand, a text response, and usually an inline artifact. Something like this:

assistant 2.5s total
ThinkingThe user wants an AuthService with key rotation. Start from the existing DB handle pattern and keep the rotation surface…0.4s
The user wants an AuthService with key rotation. Start from the existing DB handle pattern and keep the rotation surface minimal. Tests should cover rotate() first.
write_filesrc/auth.ts42 lines
// args{  "path": "src/auth.ts",  "content": "export class AuthService {\n  constructor(private db: DB) {}\n  async rotate() {}\n}"}
bashpnpm test2.1s
test output1 failed, 14 passed
FAIL  src/auth.test.ts rotate() should accept a new signer    TypeError: Cannot read properties of undefined (reading 'sign')      at AuthService.rotate (src/auth.ts:28:24)
The test expected rotate() to take a signer. I'll widen the constructor to accept one, then rewrite rotate to use it.
src/auth.tsmodified
export class AuthService {  constructor(    private readonly db: DB,    private readonly signer: Signer,  ) {}  async rotate(next: Signer) {    return next.sign(await this.db.currentKey())  }}
Baseline: a single-agent call rendered as typed blocks in a rich chat.

Every typed block is expandable on click. Thinking, tool calls with arguments, tool results. The user scans collapsed headers, opens only what matters. This is the baseline. Every serious agent product ships something shaped like it.

Now take the same problem and solve it with a workflow instead of a single call. A coder writes the draft. A tester runs the draft in parallel. A reviewer reads it. A patcher resumes the coder when the reviewer flags something. Four agents, two or three running at any moment, producing typed blocks at the same time. What do you render in the chat?

You have two real answers, and the interesting one is that you probably want both.

Option one: the ensemble

Preserve everything. Every typed block from every component, tagged with an actor, placed in the chat so the user can see that multiple agents are working in parallel. Collapsed sections let them drill in.

workflow 4 actors 4.2s total
coder1 thought · 1 tool0.6s
ThinkingStart with the AuthService skeleton, inject db via constructor. Defer rotate() until the shape settles.0.3s
Start with the AuthService skeleton, inject db via constructor. Defer rotate() until the shape settles.
write_filesrc/auth.tsfirst pass
tester2 runs3.0s
bashpnpm test --watch
first run1 failed, 14 passed
FAIL  src/auth.test.ts
  ✕ rotate() should accept a new signer
second run15 passed
PASS  src/auth.test.ts
  ✓ rotate() should accept a new signer
  ✓ 14 other tests
reviewer1 thought · 1 note0.6s
ThinkingFields should be private readonly. The rotate surface is not covered. Will flag once tests run.0.6s
Fields should be private readonly. The rotate surface is not covered. Will flag once tests run.
Suggest `private readonly` on the database field. Noting the rotate() test failure; waiting on the patcher.
Approved on second pass.
patcher1 tool0.7s
write_filesrc/auth.tswiden constructor + add signer
Ensemble view. Each actor's events grouped into a collapsible run. Click any run to expand its thinking, tool calls, and results.

The ensemble scales fidelity with the chat’s existing primitives. The patterns you reach for are the ones shadcn already gives you:

  • An actor label (name + small contrast dot) on every block header, so the reader can follow one lane visually.
  • Per-actor collapsible sections, so a four-component workflow reads as four nested accordions and expands only where the user looks.
  • A pinned status rail at the top of the message: one row per active component, showing current state, current tool call, token count.
  • A side panel with a plan tree, nodes lighting up as components run. Manus and Devin both do this.

An expert user can audit any thinking block, any tool call, any result. Nothing is lost.

The failure mode is structural. Past three or four components the chat starts to feel like a dashboard. Attention splits across lanes. A user who wanted a story gets a monitoring tool. The parallelism is visible, but the meaning is not.

Option two: the edit

Collapse the ensemble to a single voice that speaks retrospectively about what just happened.

src/auth.tsfinal
export class AuthService {  constructor(    private readonly db: DB,    private readonly signer: Signer,  ) {}  async rotate(next: Signer) {    return next.sign(await this.db.currentKey())  }}
Edit view. Narrator collapses the whole workflow to one block of prose.

One text block. First person. Past tense. The parallelism is present, carried by the word “while,” not by layout. The terminal artifact is handed over explicitly as its own block.

The narrator is a role in the workflow, not a style choice. It subscribes to the event bus on a short retrospective delay (one to two seconds is usually enough), clusters related events, and emits prose. It is one LLM call per narration window, not per event. Cheap and steady.

The tradeoffs are real. Structured evidence disappears unless you render it alongside. The narrator can smooth over failures a user should have seen: the first test run failed, the coder tried a wrong fix, the reviewer flagged a regression, the second patch worked. The narrator renders this as “I drafted, fixed, and shipped.” Three debugging cycles compressed into one clause. The ensemble shows the cycles; the edit hides them. You pay latency for the retrospective buffer. You spend tokens on a component whose only job is to talk about other components.

Neither alone is enough

The ensemble is a dashboard. The edit is a paragraph. Ship both, in the same chat, as first-class message parts. Narrator on top for the story. Ensemble below, collapsed by default, for the evidence.

ThinkingStart with the skeleton, defer rotate() until the shape settles.0.3s
Start with the skeleton, defer rotate() until the shape settles.
first run1 failed
FAIL  src/auth.test.ts rotate() should accept a signer
write_filesrc/auth.tswiden constructor + rewrite rotate
Second run: 15 passed.
src/auth.tsfinal
export class AuthService {  constructor(    private readonly db: DB,    private readonly signer: Signer,  ) {}  async rotate(next: Signer) {    return next.sign(await this.db.currentKey())  }}
Combined. Narrator for the story, ensemble collapsed below for the evidence, artifact last. One message.

With both in the message, the user picks fidelity with their eyes. Read the paragraph for the story. Expand the thinking block to see what the coder considered. Click the failing test to see why it failed. Compare the patcher’s diff to the final artifact.

This split also buys free rendering optionality. The same workflow emits one prose block and many structured blocks. An IDE renders both. A mobile chat shows only the narrator. A CI summary takes just the artifact. A debug view shows only the ensemble. Same workflow, same event bus, four different rendered products.

The message shape that makes this work

Two small schema choices are enough.

First, every typed block carries actor and parent_id. The actor is which component emitted the block. The parent is the event it causally depends on. The chat uses these fields to render accordions, lanes, or tree structure without a second schema. A narrator uses the same fields to decide which events to cluster into one sentence.

Second, narrator_text is a normal text block with a flag. The UI renders it as prose. The chat knows it is derived and can suppress, collapse, or pin it based on the view.

That is it. Two fields, one block type, and a narrator role that reads the bus. A workflow message becomes a triple: one narrator_text block, an ensemble of actor-tagged typed blocks, and the terminal artifact.

What I would build next

If you have a rich chat today and you are adding a workflow agent behind it:

  1. Stamp every emitted block with actor and parent_id. Backfill the existing single-agent blocks with a default actor so the schema is uniform.
  2. Add a narrator role that subscribes to the event bus on a short buffer and emits narrator_text blocks on a cadence. Start with a one-second buffer and tune from there.
  3. Render both by default. Narrator first, ensemble collapsed below, artifact last.
  4. Add a view toggle at the message level. Ensemble-first for power users and debugging, edit-first for everyone else.

The first time a user runs a multi-step workflow through this UI, they understand what the agent did in a second of reading (the edit), and if they care why, it is one click away (the ensemble). That gap, between story and evidence, is the thing most workflow products are missing.

The chat can hold both. Ship both.

Revision history6revisions
  1. Opus 4.7
    polish pass: stray tex formula removed, narrator-smoothing example added showing three debugging cycles compressed into one clause
  2. Opus 4.7 view trace →
    10 asst turns, 9 tool calls captured
  3. Opus 4.7
    draft — first framing around linear extensions, rejected by operator as muddled
  4. Opus 4.7
    full rewrite — scrapped order-theory angle, reframed around shadcn-chat baseline and ensemble-vs-edit as real options
  5. Opus 4.7
    richness pass: built ChatMock component, added four rendered mockups inline (baseline / ensemble / edit / combined)
  6. Opus 4.7
    ChatMock redesigned: replaced wireframe transcript with rounded-card layout, rounded-lg tool pills with SVG icon badges, run_group collapsible containers

Comments

Comments load from GitHub Discussions via Giscus. Configure PUBLIC_GISCUS_REPO, PUBLIC_GISCUS_REPO_ID, PUBLIC_GISCUS_CATEGORY, and PUBLIC_GISCUS_CATEGORY_ID in .env. See giscus.app to generate the IDs after you enable Discussions on the repo.