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.

5 min read
10 sections
Edit this page

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.

python
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

RSVP intake Live
Which of the three people who replied are not yet on the guest list?
Searched for inbox/*.eml, read 4 filesinbox/*.eml · guests.csv · msg-1.eml · msg-2.eml✓ glob_files · 4ms — 3 lines4 tools · 21.0ms
The following people who replied are not on the guest list:
Wrote summary.mdsummary.md1 tool · 3.2ms
RSVP summaryDoc · Click to open
14,317 tokens · claude-opus-5

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

CallWhat 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.

python
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

python
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

text
⌕  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, not read_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.0ms on 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:

RowDrawn when
ApprovalA side-effecting call was queued — Always approve · Deny · Approve
ConnectionThe agent asked you to connect something, with its reason
ArtifactA tool produced a file — Q2 Kickoff Brief · Doc · Click to open
Decision / ObservedProgress narration, when enabled
NoteLockdown 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:

python
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