perf(sidebar): coalesce the resize drag to one width update per frame - #131
Conversation
`SidebarResizeHandle`'s mousemove handler was raw — one `onWidthChange` per event, so roughly 60 width updates a second while dragging. Each one relayouts the workspace, which wakes every terminal's ResizeObserver, calls `fit()` and `pty.resize()`, and makes ConPTY re-emit its visible screen. A one-second drag did sixty rounds of that across every open terminal. `sidebarWidth` was also a dependency of the auto-save listener effect and the save callback, so each intermediate value re-subscribed the listener and rebuilt the callback. Reading it through a ref keeps both stable, so only the settled width matters. `createResizeDrag` collects pointer positions and emits at most one delta per frame. `stop()` flushes a position still waiting for its frame, so the width settles where the cursor was released rather than a frame behind it. The scheduler is injected rather than calling requestAnimationFrame directly, which is what makes the coalescing testable in the node-environment unit suite (7 tests, no DOM needed). Behaviour is unchanged from the user's side: the width still tracks the cursor, and dragging below 80px still auto-collapses.
Merges simplyBarbe's end-to-end audit of the renderer for user-facing text
that never reached useT()/translate(): SplitPane, Tutorial, Sidebar, every
Settings sub-panel, ErrorBoundary, OrchestrationPanel, ShortcutCheatSheet,
the Diff pane, CopyMode/FindBar/bell, Titlebar notifications, AddressBar,
CommandPalette, and the default workspace/session titles.
Also carries the markdown-file fix: read/write errors always returned a
truthy English `error` string, so the renderer's t('markdown.error.*')
fallback could never fire. A stable `code` field now travels alongside it —
CLI and pipe callers still see the English message verbatim, the renderer
maps the code to a translated one.
Conflict resolved against PR #131: the save-session notification takes the
translated string, and its dependency array stays free of sidebarWidth,
which is now read through sidebarWidthRef so a drag cannot rebuild the
callback.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Merged in 42f9655, shipping in v0.40.0. Thanks — and the framing in the description ("this is churn, not correctness") is why I merged it as-is rather than asking for changes. The cost chain you traced is the real argument: one Injecting the frame scheduler instead of calling The One note for the record: this collided with #136 on |
One sidebar drag does far more work than it needs to. Not a user-visible break on its own — this is churn, not correctness.
Cause
SidebarResizeHandle.tsxhandlesmousemoveraw, with no throttle or rAF coalescing:So ~60 width updates a second while dragging. Each one sets
sidebarWidthinApp.tsx, which relayouts the workspace — which wakes every terminal'sResizeObserver(useTerminal.ts:858), callsfit()andpty.resize(), and makes ConPTY re-emit its visible screen. A one-second drag is sixty rounds of that across every open terminal.sidebarWidthis also a dependency of the auto-save listener effect and ofhandleSaveSession, so every intermediate value tears down and re-registers the listener and rebuilds the callback.Fix
createResizeDragcollects pointer positions and emits at most one delta per frame.stop()flushes a position still waiting for its frame, so the width settles where the cursor was released rather than a frame behind it.The frame scheduler is injected rather than calling
requestAnimationFramedirectly. That's the one design decision worth pointing at: it's what makes the coalescing testable in the existing node-environment unit suite, with no DOM and no new test dependency.App.tsxreads the width through a ref instead of taking it as an effect/callback dependency, so only the settled width matters.The rAF shape matches what
useTerminal.tsalready does at:860and:980.Behaviour
Unchanged from the user's side — the width tracks the cursor, and dragging below 80px still auto-collapses. The difference is one update per frame instead of one per event.
Verification
stop()flushes the pending position,stop()cancels the frame, no emit when the pointer never moved, no double-emit.npm test→ 550 passed. The 3 failures (pty-manager,shell-context-menu) are present identically on unmodifiedmaster— both need native Windows.