Skip to content

fix(site/src/pages/AgentsPage): stabilize chat scroll on git watcher updates#23716

Draft
mafredri wants to merge 1 commit intomainfrom
fix/git-watcher-scroll-reset
Draft

fix(site/src/pages/AgentsPage): stabilize chat scroll on git watcher updates#23716
mafredri wants to merge 1 commit intomainfrom
fix/git-watcher-scroll-reset

Conversation

@mafredri
Copy link
Copy Markdown
Member

The chat scroll position jumped every time the git watcher WebSocket received data. Root cause: urlTransform was recreated on every AgentDetail render due to React Compiler hook interleaving, and isConnected state caused unnecessary re-renders on every WebSocket lifecycle event.

Changes

  1. Extract buildUrlTransform as a module-level function and pass primitive fields via urlTransformProps instead of a pre-built closure. The compiler guards the props object on primitives (stable) and guards the buildUrlTransform call inside AgentDetailView. The AgentDetailTimeline guard now hits when git data changes, keeping the scroll container content cached.

  2. Convert isConnected from useState to useRef in useGitWatcher. No component renders this value; it was causing two wasted full-tree re-renders per WebSocket open/close cycle.

  3. Split gitWatcher prop into gitRepositories and gitRefresh, and use repositoriesRef for imperative access in handleOpenInEditor. This removes the opaque gitWatcher object from the AgentDetailView JSX guard.

Diagnosis (compiler output analysis)

Before: urlTransform had no compiler cache guard (bare const in compiled output) because useProxy() between its dependencies and definition prevented the compiler from grouping the closure. The AgentDetailTimeline guard (slot 57 in AgentDetailView) depended on urlTransform, so it missed on every render, cascading a full conversation re-render into the scroll container.

After: urlTransformProps object is guarded on primitive string values (slot 157-160 in AgentDetail). Inside AgentDetailView, buildUrlTransform call is guarded (slot 5). Timeline guard depends on stable urlTransform. Compiler lint: 0 diagnostics, 188 compiled functions (was 187).

Vault patterns applied: compiler-pattern-hook-interleaving, compiler-output-is-diagnosis-not-proof, compiler-pattern-usequery-whole-object-dep.

🤖 This PR was created with the help of Coder Agents, and will be reviewed by a human. 🏂🏻

…updates

The chat scroll position jumped every time the git watcher WebSocket
received data because urlTransform was recreated on every AgentDetail
render due to React Compiler hook interleaving, and isConnected state
caused unnecessary re-renders on every WebSocket lifecycle event.

Three changes fix this:

1. Extract buildUrlTransform as a module-level function and pass
   primitive fields (proxyHost, agentName, wsName, wsOwner) via
   urlTransformProps instead of a pre-built function. The compiler
   guards the props object on primitives (stable) and guards the
   buildUrlTransform call inside AgentDetailView (slot 5). The
   AgentDetailTimeline guard (slot 59) now hits when git data
   changes, keeping the scroll container content cached.

2. Convert isConnected from useState to useRef in useGitWatcher.
   No component renders this value; it was causing two wasted
   full-tree re-renders per WebSocket open/close cycle.

3. Split the gitWatcher prop into gitRepositories and gitRefresh,
   and use repositoriesRef for imperative access in
   handleOpenInEditor. This removes the opaque gitWatcher object
   from the AgentDetailView JSX guard, preventing unnecessary
   element re-creation when only git data changes.
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