Skip to content

perf(web): workspace switches leave off-screen chats alone - #31615

Draft
flamboh wants to merge 3225 commits into
pingdotgg:maria/redesign/6-workspace-panesfrom
flamboh:t3/redesign-fix-switch-slide-stalls
Draft

flamboh wants to merge 3225 commits into
pingdotgg:maria/redesign/6-workspace-panesfrom
flamboh:t3/redesign-fix-switch-slide-stalls

Conversation

@flamboh

@flamboh flamboh commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Note

🤖 Claude Opus 5.5 on behalf of Oliver

Fix for the redesign stack, targets layer 6 (#28845).

Problem

Every workspace switch (Ctrl+Alt+U/I) re-rendered every mounted chat, even chats in workspaces the switch never touches. With four chat workspaces, one warmed switch ran ChatView 9 times and ChatComposer 11 times.

There were two causes:

  • The pane look flips for every chat. The slide gives panes their split look, and that split flag changed for every chat's pane state when the slide began and again when it landed, including chats not on screen.
  • The route flips a window-wide flag. Each switch changes the URL, so AppSidebarLayout's window-wide "suppress panel animations" flag turned on, then off two frames later. Every chat already has its own copy of that flag. But React's lazy context propagation still called every ChatView and ChatComposer that reads it, both times.

Fix

  • A chat that isn't on screen keeps a steady pane state: no split look, not narrow. It now only re-renders when it comes on screen, which re-renders it anyway.
  • Chats read only their own panel-animation flag. They no longer subscribe to the window-wide one, so its flips stop reaching them. Everything outside chats reads the window flag as before.

Numbers

Dev build with the React Compiler on, headless Chromium at 1280×800, four Replay chat workspaces (all visited) plus an empty one. Render counts come from a React commit hook. All 8 warmed switches gave the same counts:

Per switch Layer 6 This PR
ChatView renders 9 5 (only the two chats in the switch)
ChatComposer renders 11 7

The savings grow with the number of chats outside the switch. Rapid bursts improve less, because most mounted chats join the slide.

Before / after

What to look for: before, workspace shortcuts cycle through four Replay chats; after, the same sequence still works with the off-screen chat change.

https://gh-file-drop-api-prod-galwoqjslzlnws6s.oliver-boorstein.workers.dev/f/1a909bfdd8b3fcbc/31615-1x.mp4

Motion slowed 5× in the page:

https://gh-file-drop-api-prod-galwoqjslzlnws6s.oliver-boorstein.workers.dev/f/e1fd2a5cd40a6499/31615-5x.mp4

These development recordings verify shortcut behavior. They do not establish the render counts above.

Validation

  • Render counts above, measured in one isolated dev server.
  • No new test: the change is which components re-render, with no new logic.

Claude Opus 5.5 (Claude Code)

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Nov 5, 2026
@maria-rcks
maria-rcks force-pushed the maria/redesign/6-workspace-panes branch 2 times, most recently from 1ce0c0b to 9d63f6b Compare November 6, 2026 02:58
@flamboh
flamboh marked this pull request as ready for review November 6, 2026 05:13
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Nov 6, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 512efc7

Macroscope's review found this PR approvable — This change reduces unnecessary workspace-switch rendering and separates chat animation state while preserving snooze behavior. Its effects are limited to client-side presentation, with no changes to product defaults or persistent data operations.

No code changes detected at 8d8c343. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@maria-rcks
maria-rcks force-pushed the maria/redesign/6-workspace-panes branch 11 times, most recently from bfeb51d to cdb88a3 Compare November 8, 2026 22:34
@macroscopeapp
macroscopeapp Bot dismissed their stale review November 8, 2026 22:35

Dismissing prior approval to re-evaluate 22f52d4

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). size:L 100-499 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). size:XXL 1,000+ changed lines (additions + deletions). labels Nov 8, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Nov 8, 2026
@maria-rcks
maria-rcks force-pushed the maria/redesign/6-workspace-panes branch 6 times, most recently from 61b6aa9 to f434d60 Compare November 9, 2026 05:45
…arts under the tabs

The terminal pane's split and close actions move to a mirrored right notch, as a panel pane's do, and its "+" follows the last tab like the chat tab strip's. The shell drops the band and side gutter around it, so it starts right under the notch with its text in line with the tab icons.
- ThreadServersPanel, ThreadAutomationsPanel: drop the empty-card visibility cases, a visual fix proven by captures
- ThreadAutomationsPanel: drop toBeDefined asserts on elements the next line already uses
- ThreadTerminalDrawer: drop the add-to-chat menu item case, it mirrors an optional prop
- KeybindingsSettings.logic: drop the default label copy check
- settingsSearch: fold seven identical layout rows into one case
- contracts settings: drop the onboardingCompletedAt decode/encode round trip, a schema mirror
runInNewLayoutTab cleared whatever request the store held once its own
action settled, so a second cmd+N started before the first settled lost
its request and replaced the first new tab instead of adding one. Each
request now carries its own ticket and an action clears only its own.
The chip and divider drags kept their gesture in a press ref plus a drag
state, and the release read the drag from the last render. A flick whose
move had not rendered yet lost its drop: the release click appended the
pane on the right and the ghost and drop zone stayed on screen. Each
gesture is now one explicit phase the handlers read directly; the state
only draws the canvas.
A plain open (sidebar click, link, palette) lands in the focused pane's
ghost tab. A ghost or launcher is replaced in place, a kept tab gets the
ghost right after it, and a thread already open is focused where it is.
A focused surface targets its thread's chat, or opens the ghost beside
itself when that thread has no chat. A window's lone ghost looks like any
other tab, so one pane with no arranging behaves as before.

Middle-click opens a kept tab, alt-click opens to the side, and new
threads (cmd+N) still replace the focused tab. Sending a message no longer
keeps a ghost, for sidebar and lineage ghosts alike. A thread's terminal
and side panes hide when an open swaps its chat off a pane and come back
when an open brings it back. The sidebar's current row follows a focused
surface's thread. Layouts saved before make their lone chat the ghost.
Panes other than the focused one draw their active tab a tier down, in the
muted foreground, and their other tabs in the dim foreground, so the window
has one full-strength tab label. A single pane is unchanged.
Thread details reads the context breakdown from compaction.logic, where main moved the meter's helpers, and the context file keeps only the segment bar it still renders. cmd+N no longer reads the removed legacy sidebar setting, ChatView drops the compact reason nothing shows anymore, and the settings search filter wraps as the formatter expects.
…debar

Pressing Ctrl or Cmd re-rendered the whole sidebar and the composer's
send controls on keydown and again on keyup, so every chord like Mod+K
paid two full sidebar renders before its own work. Held modifiers now
live in one shared store; the sidebar and composer subscribe to a
derived boolean (hints shown after the hold delay, queue mode) and
re-render only when it flips. A quick chord never flips it.

Shortcut labels walked every keybinding per lookup (96 rules, several
lookups per render). Effective shortcuts are now computed once per
keybindings array and context. Keydown resolution skips rules for other
keys before evaluating when-clauses.

cmd+K open, keydown to list painted, median/p90 ms (10-12 runs,
interleaved with main and nightly, production builds, shared load):
  before: this 374/724, main 530/793, nightly 703/979 (load ~22)
  after:  this 262/349, main 473/569, nightly 433/527 (load ~15)
Ctrl keydown task during cmd+K: 19-42 ms -> 5.5 ms (trace).

Layer 1 (establish redesign primitives and shell); all touched lines
are main's code.
…very row

The virtualized scroll fade sets --fade-size and the mask in custom
properties, which inherit by default. Each time a fade turned on or off
(opening cmd+K, the first scroll in any faded list) Chrome restyled every
row below the scroller. Both properties are only read on the element
that sets them, so they are now registered as non-inherited, like the
surface and overview properties. Pixels are unchanged (palette open,
scrolled, typed and model picker screenshots are byte-identical).

cmd+K open, frame after the list renders (trace, same machine):
  ResizeObserver pass 38-42 ms -> 18.5 ms
  restyled elements 835 -> 319

Layer 1 (establish redesign primitives and shell); the fade utility is
main's code.
…eline

Every chat view render re-rendered ChatComposer because `setThreadError` was a
new function each render, and every canvas measurement re-rendered the whole
MessagesTimeline because it read `registerTimeline` from the canvas layout
context. `setThreadError` is now a stable callback, and the timeline reads its
register function from its own context, so canvas resizes reach only the
details card and the preview mini player.

Stack layer 1 (feat(web): establish redesign primitives and shell); the code is
from main.

Measured with the layer 2 footer and banner commits that follow (prod build,
trusted input, 12 samples each, interleaved; median / p90 ms):
- details panel open: 132.5 / 191.9 -> 99.5 / 130.2 (main 95.9 / 142.8, nightly 94.2 / 134.2)
- thread switch, short: 380.2 / 428.6 -> 308.8 / 445.1 (main 298.4 / 356.9, nightly 303.5 / 357.9)
- thread switch, long: 449.9 / 608.1 -> 408.8 / 502.3 (main 449.8 / 529.5, nightly 488.0 / 741.4)
Details toggle commits render 138 + 33 components instead of 182 + 106 for open
and 32 instead of 76 + 75 for close.
…oss chat view renders

The timeline footer was a new element on every chat view render, because the
compiler leaves an element unmemoized when it depends on the provider list or
the resolved handoff, so every ChatView render re-rendered the whole
MessagesTimeline. The footer is now a TimelineFooter component that reads the
providers and resolves the handoff itself, built outside the condition where
the compiler memoizes it.

An empty composer banner list was a new array each render, which re-created the
ChatComposer element. The empty case now uses the shared constant before the
element is built.

Stack layer 2 (feat(web): redesign composer and conversation timeline); the
footer shape and the tray banner list come from that layer.

Measured together with the layer 1 commit before it (prod build, trusted input,
12 samples each, interleaved; median / p90 ms):
- details panel open: 132.5 / 191.9 -> 99.5 / 130.2 (main 95.9 / 142.8, nightly 94.2 / 134.2)
- thread switch, short: 380.2 / 428.6 -> 308.8 / 445.1 (main 298.4 / 356.9, nightly 303.5 / 357.9)
- thread switch, long: 449.9 / 608.1 -> 408.8 / 502.3 (main 449.8 / 529.5, nightly 488.0 / 741.4)
…red every frame

The terminal hint was positioned from the host by measuring the focused pane,
host and composer on every animation frame through the browser surface
tracker while the terminal pane slid in, and it still trailed the pane by a
frame.

The terminal hint now renders inside the focused pane, which slides in on
top, where its resting classes already center it, so the pane's own
transform carries it. Only a lift above a taller composer is measured, once
on mount and on resize. Column and workspace hints keep the host placement:
their pane can pass behind the panes it crosses, and the hint stays above.

Stack layer 6 (feat(web): add workspace panes and transitions).

Main-thread CPU in the 700 ms after each open (trace, 2x6 opens each):
  before 400-443 ms, 56-57 forced layouts
  after  255-260 ms, 21-22 forced layouts
  main   180 ms, 12 forced layouts

Terminal open wall-clock (first frame with the terminal on screen) is
unchanged within noise; the saving is in the frames of the slide after it.
Interleaved, n=30 each, median / p90 ms, three runs:
  before 209.6 / 242.3, after 203.3 / 239.3 (main 185.6, nightly 183.0)
  before 245.1 / 268.4, after 228.9 / 260.6 (main 193.9, nightly 194.4)
  before 209.1 / 242.4, after 211.3 / 248.4 (main 176.2, nightly 187.4)
Thread switch (short), n=30: 350.1 -> 340.3 ms, unchanged within noise.
cmd+D toggles the diff where Settings > Layout puts the side pane, as a normal
tab, instead of opening an empty launcher pane. A diff tab hidden behind
another tab comes forward. A new pane to the right moves to cmd+\, and a saved
copy of the old cmd+D binding is dropped on startup.
@maria-rcks
maria-rcks force-pushed the maria/redesign/6-workspace-panes branch from 8673be9 to 94e50e3 Compare November 9, 2026 09:10
@maria-rcks
maria-rcks force-pushed the maria/redesign/6-workspace-panes branch from 94e50e3 to e976636 Compare November 9, 2026 09:28

@juliusmarminge juliusmarminge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @flambo for measuring the workspace-switch rendering. #29150, merged as a0056dddb1, already stabilizes the sidebar snooze callback. Once the redesign base includes that fix, please cut this PR down to the separate off-screen chat and panel-animation changes.

@flamboh flamboh changed the title perf(web): workspace switches leave off-screen chats and unchanged sidebar rows alone perf(web): workspace switches leave off-screen chats alone Nov 9, 2026
@flamboh

flamboh commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Note

🤖 Claude Opus 5.5 on behalf of Oliver

@juliusmarminge thanks, the sidebar part is removed. This PR is now only the off-screen chat and panel-animation changes. The #29150 port for the redesign is in #33528 (onto layer 1).

@github-actions github-actions Bot added size:S 10-29 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Nov 9, 2026
@flamboh
flamboh marked this pull request as draft November 10, 2026 01:06

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 10-29 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants