refactor(ui): move workbar resizing onto Astryx Resizable - #1863
Merged
Conversation
The workbar ran a parallel implementation of useResizable/ResizeHandle: hand-rolled pointermove math in app-shell-layout-actions.ts, hand-rolled clamping and localStorage in session-workbar-layout.ts, and a hand-rolled separator in chat-workbar.tsx. useResizable now owns the width — clamping, drag, arrow-key sizing and persistence — so the actions module, its keyboard step math, the width state in useShellLayout, the debounced persistence effect and the handle's CSS all go away. The legacy maka-session-workbar-width-v1 key is deliberately not migrated. Migrating it would mean writing into Astryx's private storage slot and carrying the code plus its tests forever, to save users a single drag; a stored width resets once instead. pillPlacement="center" works around an Astryx 0.2.0 bug where a side-placed horizontal grab zone gets translateY(-50%) on top of top/bottom: 0, leaving only the divider's upper half grabbable. The workbar journey now pins the sizing contract that fails silently: arrow-key resizing, the isReversed direction, and the min/max bounds.
Astro-Han
force-pushed
the
refactor/ui-workbar-astryx-resizable
branch
from
August 1, 2026 18:32
1e3ce4a to
8830cb1
Compare
…mirror
The workbar journey asserted only aria-* on the separator, which mirrors
useResizable state rather than the rendered panel. Mutation-checked: hard-coding
`width={400}` in app-shell.tsx severs width state from render and every
assertion still passed.
Adds a computed-width assertion, plus aria-orientation — a `direction="vertical"`
typo keeps arrow keys working (ArrowLeft falls through the ArrowUp branch) while
silently breaking mouse drag, so neither the width nor the valuenow assertion
catches it. Both mutations now fail the test.
Also drops a stale factory count from the stable-actions contract comment and
untangles the useShellLayout doc comment, which claimed every value routes
through useAppShellPersistenceEffects and then immediately contradicted itself.
…ffect Astryx's autoSaveId persists from an effect keyed on size, and ResizeHandle drives _onResizeMove on every pointermove — measured at 90 synchronous localStorage writes for a single drag gesture. app-shell-effects.ts already carries the note from when that exact bug was fixed for the sidebar resizer. useResizable keeps clamping and pointer/keyboard resizing; storage moves back onto the same 200ms trailing debounce as the session list, seeded through defaultSize. Measured after: 1 write per drag. This also retires the reset-once decision. The width stays on maka-session-workbar-width-v1, so existing widths survive, no key is orphaned, and the app keeps one storage convention instead of two. readSessionWorkbarWidth deliberately does not clamp — useResizable clamps what it is handed, so a second clamp would duplicate authority over the bounds. Verified: a stored 9999 hydrates to 600.
clampSize bounds the size but does not round it, and the old clampSessionWorkbarWidth did both. Deleting it in favour of the hook's clamping dropped the rounding with it, so a sub-pixel pointer delta reached the CSS variable and localStorage as 479.70001220703125. Rounds once, where the width leaves useShellLayout, so both consumers are covered. aria-valuenow still shows the raw float: ResizeHandle renders it straight from _size, which is Astryx's to fix and affects SideNav equally. Also drops a duplicated rationale comment from app-shell-effects.ts — the decision it explains lives at the useResizable call site — and tightens the E2E comment block.
Rounding `workbar.size` on the way out left the hook holding a fractional width, which ResizeHandle mirrors straight into `aria-valuenow`: a 20.4px drag announced 350.4000244140625 while the panel measured 350px. Rounding the pointer delta instead keeps the hook's own state integral, so the CSS variable, storage and the announced value all report one number. `_onResizeMove` receives the distance from pointerdown rather than a per-move increment, so this quantises the drag without accumulating drift. Pinned by a pointer drag in the workbar journey, which also covers the `pillPlacement` workaround that had no drag exercising it.
Astryx ends a drag on pointerup, pointercancel or unmount, but not on focus loss. Cmd+Tab mid-drag and release outside the app leaves its window listeners attached and `body` stuck at `cursor: col-resize; user-select: none`, so the panel keeps tracking a button-less pointer and nothing in the UI is selectable until the user clicks somewhere. The hand-rolled resize this branch deleted cleaned up on blur. Routing blur into Astryx's own cancel path restores that without re-growing a second teardown alongside the library's.
`useResizable` clamps the `defaultSize` it is handed but never rounds it, and the deleted `clampSessionWorkbarWidth` did both. A fractional stored width therefore survived hydration and reached the panel, storage and `aria-valuenow` unchanged until the next drag. Clamping stays the hook's job — a second bound here would duplicate authority over min/max. Rounding does not: this is the one entry point reading a width the app did not produce, and every other entry already keeps it integral. The read functions had no coverage on main either; the new test pins rounding, the deliberate absence of clamping, and the invalid-value fallbacks.
Which key the width lands on is the load-bearing decision of #1861 — Astryx's `autoSaveId` writes synchronously on every committed size, ~90 writes per drag — and it was the one part of the change no assertion touched. Switching to `autoSaveId` orphaned the stored width while every existing sizing assertion stayed green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The session workbar ran a parallel implementation of what
useResizable/ResizeHandlealready provide: hand-rolledpointermovesizing inapp-shell-layout-actions.ts, hand-rolled clamping insession-workbar-layout.ts, and a hand-rolledrole="separator"div inchat-workbar.tsx.useResizablenow owns clamping and drag/keyboard sizing. That deletesapp-shell-layout-actions.tsand its test, theworkbarWidthstate inuseShellLayout, and the handle's CSS.ChatWorkbartakes aResizablePropsregion instead of two event handlers.Refs #1861 (slice 2 of 4). Scope is resize only: tabs, the artifact pane and the chat surface are untouched.
Persistence stays where it is, not on
autoSaveIdThe issue frames the storage choice as "migrate
maka-session-workbar-width-v1intoautoSaveId, or let stored widths reset once". Both options assumeautoSaveId, and measurement ruled it out.Astryx persists from an effect keyed on
size(useResizable.js:121), andResizeHandlecalls_onResizeMoveon everypointermove(ResizeHandle.js:314). Measured in the live app by spyingStorage.prototype.setItem: one drag gesture produced 90 synchronouslocalStoragewrites.app-shell-effects.ts:131already carries the note from when this exact bug was fixed for the sidebar resizer (PR-FE-BUG-HUNT-5), and the offending effect lives inside the library, so it cannot be debounced from the outside.So
useResizablekeeps clamping and resizing, and the existing 200ms debounced effect keeps the storage — seeded back throughdefaultSize. Measured after: 1 write per drag.app-shell-effects.tsends up byte-identical tomain. This also matches whatSessionListPanelalready does (packages/ui/src/session-list-panel.tsx:78passesresizablewithoutautoSaveId), so the app keeps one storage convention rather than gaining a secondastryx-resizable:namespace.Consequence: the width stays on
maka-session-workbar-width-v1, so existing widths survive, nothing resets, and no key is orphaned. The reset-once question the issue raises no longer applies.Two deliberate splits of responsibility between
session-workbar-layout.tsanduseResizable:readSessionWorkbarWidthdoes not clamp; a second bound here would duplicate authority over min/max. Verified: a stored9999hydrates to 600.clampSizebounds without rounding. On the way in, a fractional stored width would otherwise survive hydration untouched; on the way out, a sub-pixel pointer delta would reach the CSS variable and storage as479.70001220703125. Rounding_onResizeMove's delta is safe because that delta is the distance from where the pointer went down, not a per-move increment (useResizable.tsconsumes it asdragStartSizeRef.current + delta), so quantising it cannot drift or create a dead zone.Note on the keyboard
The issue describes the hand-rolled handle as "minus keyboard resizing", but it already had
onWorkbarResizeHandleKeyDownwith arrow/Home/End handling. The real win is that the step math,Enter-to-collapse, RTL handling and focus styling are no longer ours to maintain — not that keyboard resizing is new.Review focus
pillPlacement="center"is load-bearing, not styling. Astryx offsets a side-placed horizontal grab zone withtranslateY(-50%)layered ontop: 0; bottom: 0, which lifts the hit area half its height off the divider — measured live, the grab zone covered onlyy 36–428of any 36–820handle, leaving the lower half undraggable. Centering keeps it full-height. Still unfixed on astryx HEAD; verify upstream before deleting the prop.Landed from review
Findings from two independent review rounds, each checked against library source before acting and each mutation-verified. The first two were listed as known gaps in an earlier revision of this description; they are fixed, not deferred.
aria-valuenowexposed a raw float. Rounding used to live at the consumer, so the width existed in two versions — the hook's own state versus the CSS variable and storage — andapp-shell.tsxhanded both to the same component.ResizeHandlemirrors_sizestraight intoaria-valuenow, so a screen reader announced479.70001220703125for a 480px panel. Moving the rounding to_onResizeMovecollapses that to one number and is a net deletion.window.blur; Astryx ends a drag only onpointerup/pointercancel/ unmount (ResizeHandle.tsx:418-425). Cmd-Tab mid-drag and release outside the app left the listeners attached andbodystuck atcursor: col-resize; user-select: none.useShellLayoutnow bridgesblurinto Astryx's ownpointercancelpath — those listeners exist only during a drag, so it is a no-op otherwise, and no second teardown path is re-grown.clampSessionWorkbarWidthrounded and clamped; dropping it in favour of the hook's bounds dropped the rounding with it, so a fractional stored width reached the panel, storage andaria-valuenowunchanged until the next drag.readSessionWorkbarWidthrounds again. These read functions had no test onmaineither — the new unit test pins the rounding, the deliberate absence of clamping, and the invalid-value fallbacks.pillPlacement="center"carries a six-line comment about an upstream bug and survived deletion with a green suite, as did the rounding fix; one pointer drag, grabbed at 90% of the handle's height with a fractional delta, kills both. Separately, which storage key wins — the central decision of this PR — was asserted nowhere, so switching toautoSaveIdorphaned the stored width while every sizing assertion stayed green.Verification
npm run lint,npm run format:check,npm run typecheck,npm run build— clean.npm run -w apps/desktop test— 1304 pass / 0 fail. Full desktop Playwright suite — 69 passed.session-workbar.spec.tspins the sizing contract. Each assertion targets a specific silent failure — wrong config that still compiles and still renders — and each is mutation-checked:aria-orientation="vertical"direction="vertical"— arrow keys keep working (ArrowLeftfalls through theArrowUpbranch) while mouse drag breaks entirelyaria-valuenow400 → 410 onArrowLeftisReversed— direction inverts, gives 390width: 410pxwidth={400}— severs width state from render; every ARIA assertion still passed before this line existedwidth: 431pxpillPlacement="center"— the lower half of the divider stops responding to the pointeraria-valuenow431 after a 20.5px dragMath.roundon_onResizeMove— the panel reads 431px while the screen reader announces 430.5user-selecthold acrossblurmid-dragbodystays unselectablemaka-session-workbar-width-v1is431, noastryx-resizable:keyautoSaveId— the user's stored width is orphaned, every assertion above stays greenreadSessionWorkbarWidth('400.5')→401aria-valuenowon restartBehaviour measured in the live Electron app (throwaway specs, deleted before commit):
{x: 832, y: 36, w: 16, h: 784}against a handle of{x: 840, y: 36, h: 784}: full-height, centered on the divider, 16px wide vs. the 8px::afterit replaces.ArrowRight590,ArrowLeft600,Shift+ArrowRight550,Shift+ArrowLeft600,Home320,End600.maka-session-workbar-width-v1, with noastryx-resizable:key created. Killed the Electron process and relaunched against the same userData dir: width preserved.--maka-session-workbar-width: 480px, a stored480, and anaria-valuenowof480.Appearance changes, both from Astryx's own styles rather than ours:
var(--ring)line. Different shape, consistent with every other Astryx resize surface.:focus-visiblegives a 2px--color-accentoutline (ResizeHandle.tsx:93), stronger than the 1px--ringline it replaces.Not run: the narrow-breakpoint hide is still only covered by the existing
chat-shell-layout-contractCSS assertions, not by a live measurement.Known gaps, not in this PR
All upstream or pre-existing; none are introduced here.
ArrowUp/ArrowDown. Astryx foldsArrowDownintoArrowRightandArrowUpintoArrowLeftand callspreventDefault(ResizeHandle.tsx:439-455), so the workbar resizes on the vertical arrows too; the window splitter pattern reserves those for a horizontal splitter. The deleted handler ignored them, so this is a behaviour change — but it is not fixable from the call site:handleKeyDownruns unconditionally after any consumeronKeyDownand does not checkdefaultPrevented(ResizeHandle.tsx:569-571). Intercepting it would mean wrapping the handle in exactly the kind of parallel implementation this PR removes. Affects every Astryx resize surface in the app equally; belongs upstream, alongside thepillPlacementbug.session-workbar-layout.ts:3-5and again inchat-detail.css:24-26. RaisingSESSION_WORKBAR_MAX_WIDTHalone would be silently capped by the CSS.SessionWorkbarFallbacknever received a width onmaineither.SessionListPaneldoes not round itsonWidthChangevalue, somaka-chat-list-width-v1can hold the same kind of float this PR just fixed for the workbar.SideNavshares the unroundedaria-valuenowfor the same reason.