Agent pane: an always-on flicker log in a small ring file, matched to UTC time

Merged
TE
2d ago
+503-0

Leo, 2026-11-04, on his agent chat: "it's because of an entire re-render of the thing ... a flicker across nearly the full screen ... when there's a different kind of thinking or toggle ... when text is streaming in".

A replay of his chat (WebKit and Chromium, MutationObserver plus layout-shift observer) showed no transcript remount, no scroller swap and no layout shift over 0.05, so the flash is not in the DOM we can replay. This adds an always-on detector to his build, so the next flash he mentions can be matched to its time.

What it records

One JSON line per event, at in UTC, in <Application Support>/<bundle id>/diagnostics/agent-page-flicker.ndjson (a two-file ring: rolled to agent-page-flicker.1.ndjson past 256 KB).

kindfromwhen
web-content-endedhost (AgentPageTerminations.ended)the page's WebContent process ended (reason, pid, page, engine)
navigationhost (didCommit)the pane's main frame committed a document: a load or reload
window-occlusionhost (one app-wide observer)a window became visible or occluded
page-loadpagethe document loaded, with its navigation type (navigate, reload)
visibilitypagethe document became hidden or visible
transcript-mountpage (VirtualTranscript mount)the transcript root mounted; n over 1 on one page is a remount
long-framepagea frame over 100 ms while visible

Cost

  • No UI and nothing on the main thread's disk path: writes run on a utility queue.
  • Long frames come from a long-animation-frame observer where the engine has one. Otherwise (WebKit) frames are timed only for one second after a DOM change: a MutationObserver arms a frame watch, which stops itself. It never runs a rAF loop at idle.
  • The page batches events at most once a second, on the pane's own WebKit handler only (diag.flicker, answered in AgentPaneBridge before AgentPaneRequest, not logged). Gallery, tests and the shared page host send nothing.
  • Nothing of the chat's text is written: kinds, numbers, short words and the page name (chat:<session> or new-tab). The host keeps only known kinds and short scalar fields from the page, at most 32 events per call.

Tests

  • webviews/test/flicker-log.test.ts: batching with times, mount counts, a dropped burst, and frame timing only while changing and visible, timed from the change.
  • AgentPageFlickerLogTests: UTC time per line, the page filter, the ring rollover.

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds an always-on detector that logs what can make the agent pane flash, so the next flicker Leo reports can be matched to its time. The pane writes one JSON line per event, with its time in UTC, to <Application Support>/<bundle id>/diagnostics/agent-page-flicker.ndjson, a two-file ring that rolls over past 256 KB.

What it records

  • From the host: the page's WebContent process ended, the main frame committed a document, and any window's occlusion flip.
  • From the page, via the pane's own WebKit handler: page load with its navigation type, visibility flips, transcript remounts, and frames over 100 ms while visible.
  • Nothing of the chat's text is written; page events are kept to known kinds and short scalar fields.

Cost and limits

  • Writes run on a utility queue, with no UI.
  • Long frames come from a long-animation-frame observer when the engine supports and accepts one; otherwise frames are timed only for one second after a DOM change, never at idle. A detector that cannot start never stops the pane from mounting.
  • The page batches events at most once a second, on the pane's own WebKit handler only.