Agent pane: an always-on flicker log in a small ring file, matched to UTC time
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).
| kind | from | when |
|---|---|---|
web-content-ended | host (AgentPageTerminations.ended) | the page's WebContent process ended (reason, pid, page, engine) |
navigation | host (didCommit) | the pane's main frame committed a document: a load or reload |
window-occlusion | host (one app-wide observer) | a window became visible or occluded |
page-load | page | the document loaded, with its navigation type (navigate, reload) |
visibility | page | the document became hidden or visible |
transcript-mount | page (VirtualTranscript mount) | the transcript root mounted; n over 1 on one page is a remount |
long-frame | page | a 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-frameobserver 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 inAgentPaneBridgebeforeAgentPaneRequest, 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>ornew-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](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
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-frameobserver 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.