Skip to content

perf(webui): bounded transcript DOM for long sessions - #329

Merged
jkyberneees merged 4 commits into
mainfrom
perf/webui-long-sessions
Oct 17, 2026
Merged

jkyberneees merged 4 commits into
mainfrom
perf/webui-long-sessions

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

What

Long WebUI sessions no longer slow down as the transcript grows.

The old 80-item cap only counted top-level rows. One turn's stream, holding hundreds of tool steps and each step's eagerly built result view, was never trimmed. Opening a large session also rendered every message before pruning.

  • Transcript budget (js/transcript.js, new)
    • Caps the transcript at 300 rows, counting the rows inside turn streams.
    • Live work is never trimmed: the streaming answer, open reasoning, running sub-agents, and tool steps still waiting for a result.
    • While you are scrolled up reading, trimming waits until 600 rows. It then removes only rows wholly above the screen, and the view stays put. Past 1,200 rows the oldest rows go regardless.
    • A note at the top of the transcript says when rows are hidden.
  • Windowed session load
    • Opening a session renders its newest ~150 rows.
    • Show earlier renders the previous window above, in order, with a split turn rejoined, and the visible rows don't move.
    • Rows of a loaded message are trimmed together, and the backlog resumes at the first remaining message.
    • Rows trimmed from a live turn come back via Reload earlier history once the turn ends. The reload re-renders only the transcript; the connection, queue and plan are untouched.
    • Tool results outside the window still reach the Activity/evidence panel, in transcript order.
  • Lazy tool results: a step's result view (toolbar, search, line rows) is built on first expand.
  • Fold measurement: older answers are measured once instead of on every new reply. Previously each new reply forced a layout per assistant bubble.
  • Row caps: reasoning blocks keep their newest 400 lines and sub-agent chip logs their newest 200. Older lines are summarized in a note.
  • Activity list: repaints once per frame, keeps a running byte total and caches each result's kind.

Measured (headless Chromium, odek serve, synthetic session)

main this PR
Open a 600-turn session (3,601 messages) 18.5 s, 10,855 nodes 0.43 s, 2,606 nodes
Live turn, 1,000 tool steps: transcript nodes 67,001 3,004
Live turn, frame-paced step time, steps 100 → 1,000 16 → 30 ms (below 60 fps) flat ~16 ms

Browser checks:

  • Show earlier while scrolled mid-transcript: a visible row stays at the same offset (1981 → 1981 px).
  • Trim at the reading ceiling: the row being read stays at the same offset (383 → 383 px).
  • Message order after paging back is complete and ascending, older long answers fold, and there are no console errors.

Tests

  • New cmd/odek/ui/js/transcript.test.js (32 tests). Every regression test was RED on the code before its fix; the one exception is a live-work invariant that already held.
  • Full JS suite: 379/379 pass. go test ./cmd/odek WebUI tests pass, including the JS↔CSS class contract.
  • Line coverage:
File Before After
render.js 85.5% 89.8%
workspace.js 47.9% 74.5%
utils.js 68.1% 74.1%
sessions.js 67.1% 68.3%
transcript.js new 99.0%
  • Three adversarial review passes (an independent first-commit review, /code-review high, /code-review medium). Every confirmed finding is fixed in commits 2–4, each with a regression test.
  • Two findings were declined:
    • Ctrl-F cannot see unexpanded tool output, but collapsed .tb-body was already display:none, so this is unchanged.
    • The parsed session is kept for Show earlier. That is bounded by the session-file cap and needed to recover trimmed rows.
  • Every browser scenario above was re-checked after each round.

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek d910baa Commit Preview URL

Branch Preview URL
Oct 17 2026, 08:20 PM

jkyberneees and others added 4 commits October 17, 2026 22:19
The transcript grew without bound in long sessions: the 80-item cap only
counted top-level rows, so one turn's stream (hundreds of tool steps, each
with an eagerly rendered result view) was never trimmed, and opening a
large session rendered every message before pruning.

- js/transcript.js: a 300-row budget that counts rows inside turn streams,
  never trims live work, defers while the operator reads up (600-row
  ceiling, scroll position held) and shows a top-of-transcript note.
- Session load renders the newest ~150-row window; "Show earlier" renders
  the previous window in place. Trimmed history rows return to the
  backlog as whole messages; rows trimmed from a live turn come back via
  a session reload once the turn ends.
- Tool result views render on first expand.
- Older answers are measured for folding once instead of on every reply.
- Reasoning blocks keep their newest 400 lines; sub-agent chip logs 200.
- The Activity list repaints once per frame and tracks its byte total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the transcript budget:
- Scroll correction measured a surviving row instead of the height delta.
  Chrome and Firefox scroll anchoring had already compensated, so Show
  earlier while scrolled mid-transcript jumped by the inserted height.
- While reading up, the ceiling trim removes only rows wholly above the
  view (never splitting a loaded message); 1,200 rows is the hard bound.
- Steps settled by a cancelled or interrupted turn are no longer treated
  as running, so the budget can trim them.
- Steps re-rendered by Show earlier keep their Inspect result action.
- The marker text is a polite live region, written only when it changes;
  focus moves into the transcript when the last window removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- History from a session that fit one window stays recoverable after the
  budget trims it (the backlog is kept even when nothing was hidden).
- Show earlier holds the first row on screen when the older half of a
  split turn joins its stream (the stream's own top does not move; Safari
  has no scroll anchoring to hide this).
- Activity items recorded at load are rebound to steps Show earlier
  renders, so "Show in conversation" reaches them.
- Several results on one unopened step all render on expand.
- A bubble measured at zero height (hidden pane) is measured again.
- System-initiated user records use no window budget and never open a
  window.
- The in-view search while reading up is a binary search.
- Reload from the trim marker opens at the top of its window.
- hasClass/nodeHolds live in utils.js; message and reasoning-note markup
  each have one builder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An interrupted turn clears the live flag on its reasoning row, so the
  budget can trim it.
- Folds are re-measured after a layout change (window or inspector
  width, reading size, density), debounced.
- Identical results within one turn rebind to distinct Activity items.
- The earlier-turns count skips system-initiated records.
- Session load collects only the newest results the Activity panel can
  hold, walking back from the window: load cost no longer grows with the
  session's tool history.
- A reader parked where nothing above the view can go costs no layout
  reads until they scroll.
- The trim loop stops once a message boundary is reached.
- Reload re-renders the transcript only (no session_switch, queue pause,
  plan or Activity reset) and rebinds the existing Activity items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jkyberneees
jkyberneees force-pushed the perf/webui-long-sessions branch from 6ece589 to d910baa Compare October 17, 2026 20:19
@jkyberneees
jkyberneees merged commit ba66fdf into main Oct 17, 2026
10 checks passed
@jkyberneees
jkyberneees deleted the perf/webui-long-sessions branch October 17, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant