The live UI panel
A chat card that redraws in place while the agent works — tokens, tool rows with real output, approval and artifact cards, tokens footer.
agent.run_live() prints a transcript to a terminal. watch() draws the
same run as an HTML card that redraws in place — in a Jupyter cell, or
anywhere you can render HTML.
from shipit_agent.narrate import watch
answer = watch(agent, "Which accounts are at risk this quarter?")While the run proceeds the card updates: tokens land with a caret, tool rows appear in flight and settle, each call folds away with the bytes it actually returned, approval and artifact cards interrupt the flow, and the footer counts tokens.
It is fed the same WorkRunAccumulator as every other renderer, so a row
reads identically in the terminal, in a shared HTML file and here. Only the
shape changes.
What it looks like
Each row above is one thing the runtime emitted. Click a ✓ glob_files · 4ms
line in a real panel and the tool's actual output opens underneath it.
The three surfaces
| Call | What you get |
|---|---|
watch(agent, prompt) | Live card, redrawing; returns the final answer |
watch(agent, prompt, shape="tree") | The same, drawn as a tree |
render_chat_html(events) | A finished run as an HTML fragment |
render_chat_html takes an AgentResult, a list of events, or anything
iterable of events, and returns a self-contained fragment (styles included)
you can drop into a page.
from shipit_agent.narrate import render_chat_html
result = agent.run("Summarise the inbox")
html = render_chat_html(result.events, model="claude-opus-5", title="Inbox")Options
watch(
agent,
prompt,
title="RSVP intake", # heading on the card
shape="chat", # or "tree"
show_output=True, # fold each call's real output behind it
output_limit=None, # None = every byte; default 4000 chars
min_interval=0.05, # redraw throttle for token deltas
)output_limit=None
The default clips a tool's output at 4,000 characters. None shows every
byte — each output scrolls in its own box, so a long file costs height inside
that box rather than in the page.
Throttling
A full re-render per token is O(n²) DOM churn, and it stutters exactly when
the answer gets long. Structural events — a call starting, an approval
landing, the run finishing — always flush immediately; token deltas flush on
a time budget. min_interval=0 disables the throttle if you want every
token painted.
What a row looks like
⌕ Read 3 files, searched for renewal_date 3 tools · 14.0ms
accounts.csv · tickets.csv · usage.csv
✓ read_file · 4ms — 128 lines ← click to open the real output- Verb and target, not a tool name —
Read accounts.csv, notread_file({"path": …}). See the Narrator. - Consecutive calls collapse into one row. Prose breaks the run; so does a tool group the runtime declared.
3 tools · 14.0mson the right: how many calls the row stands for and how long they took together.- Each call folds away with its real output behind it. Nothing is summarised — the bytes are there.
Cards
Some rows are not work. They interrupt, because they are the thing you have to read in order to answer:
| Row | Drawn when |
|---|---|
| Approval | A side-effecting call was queued — Always approve · Deny · Approve |
| Connection | The agent asked you to connect something, with its reason |
| Artifact | A tool produced a file — Q2 Kickoff Brief · Doc · Click to open |
| Decision / Observed | Progress narration, when enabled |
| Note | Lockdown engaged, context compacted, guardrail triggered |
See apps for artifacts and connections for the connect card.
Styling and safety
Every selector is scoped under .sa-live. Notebook output shares one
document with the whole page, and a bare body { … } rule would restyle the
user's notebook; a test walks the stylesheet to keep it that way.
The card is theme-aware — it follows prefers-color-scheme — and every value
that comes from a model, a tool or a filename is HTML-escaped.
Using it outside a notebook
LiveView is the class behind watch. With display=False it never touches
IPython, so it works in a web app, a test, or a script:
from shipit_agent.narrate import LiveView
view = LiveView(prompt=prompt, model="claude-opus-5", display=False)
for event in agent.stream(prompt):
view.feed(event)
socket.send(view.html()) # or push only on structural events
view.close()For a frontend you usually want the timeline instead — JSON steps rather than rendered HTML.
See also
- The tree view — the shape of a run rather than its prose
- The UI timeline — the events a frontend draws
- Progress narration — decisions and observations
- Apps — artifacts, and the cards they produce