fix(desktop): rebuild the workbar shell on TabList and one open/close control - #4789
Open
Astro-Han wants to merge 1 commit into
Open
fix(desktop): rebuild the workbar shell on TabList and one open/close control#4789Astro-Han wants to merge 1 commit into
Astro-Han wants to merge 1 commit into
Conversation
Astro-Han
force-pushed
the
fix/desktop-workbar-tab-strip-on-tablist
branch
2 times, most recently
from
September 4, 2026 15:08
e4f7996 to
6885bdb
Compare
15 tasks
Astro-Han
force-pushed
the
fix/desktop-workbar-tab-strip-on-tablist
branch
4 times, most recently
from
September 4, 2026 18:10
0387e62 to
ef9ab30
Compare
… control The right workbar drew its own tab strip: a hand-written `role="tablist"` div of ghost `Button`s with a close `IconButton` beside each, dnd-kit drag reorder, a context menu of move/close verbs, and a preview/pin state no renderer code ever set. It read as a browser tab bar without being one, and the strip's container had no `min-width: 0`, so at the panel's floor the tabs spilled past the edge and pushed [+] and the collapse toggle off screen instead of scrolling. The strip is now Astryx's `TabList`. That settles what the hand-written one could not decide: - `Tab` renders `endContent` inside its own `<button>`, so a per-tab close would nest a button in a button, and `TabList` warns when a `role=tablist` strip's direct children are not tabs. Opening and closing therefore share the `[+]` menu, which lists every registered tool and marks the open ones with a checkmark — the row-end mark `Selector` documents as the house convention, not a boxed checkbox, and with no shortcut column, since the launcher below already lists every face with its own. - Nothing reorders tabs any more, so the strip's order is the order the faces were opened in, and dnd-kit leaves the dependency tree with it. - The strip lives in a `min-width: 0` flex item and takes its height from the titlebar clearance it stands in, never the other way round. - The rail under the tabs belongs to the bar, not to the strip. `TabList hasDivider` draws it on the strip's own box, which ends where the tabs do, so it ran out from under `[+]` and the collapse toggle and read as a line beneath half a row. `Tab` names a second idiom for exactly this — a toolbar with a bottom divider, handing the tabs `--_tab-indicator-bottom` so the selected indicator drops through the bar's gap and lands on the rail — and that is what the bar does now. Alongside the shell: - The Task face is retired; its ledger is folded into Work Board separately. The renderer's read path goes with it — the hook, `SessionTodoPanel`, its stylesheet, its shared copy, and `WorkbarTodoService`, which had no caller left once the hook went. `todo:read` and `querySessionTodo` stay: that is the boundary Work Board will read through, and it carries its own projection tests. - Review carried `git-branch`, which read as "switch branch". The face lists changed files and their diffs, so it carries `file-diff`. - `WORKBAR_TOOL_DEFINITIONS` is now the only place a face's icon, shortcut and order are written. The strip, the `[+]` menu and the launcher all read it instead of keeping three hand-maintained lists in the surface. - The panel's floor rises from 320 to 340, the width `astryx docs layout` budgets for a detail panel and the point where five tabs stop fitting (386px needed against 260px available). The `turn-narrative` e2e fixture opened the workbar on the Task face and seeded a to-do ledger for it. The scenario is about focus order through the transcript, so it opens 变更 instead and no longer seeds what nothing renders. The workbar story frame declares `--maka-plate-titlebar-clearance`, which AppShell puts on the frame holding the plates. Without it the bar's grid row collapsed and the strip floated mid-panel, so the story could not show the shell it exists to pin. Refs #2188 Generated-by: Claude Code
Astro-Han
force-pushed
the
fix/desktop-workbar-tab-strip-on-tablist
branch
from
September 4, 2026 19:00
ef9ab30 to
68881d6
Compare
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 right workbar drew its own tab strip: a hand-written
role="tablist"div of ghostButtons with a closeIconButtonbeside each, dnd-kit drag reorder, a context menu of move/close verbs, and a preview/pin state no renderer code ever set. It read as a browser tab bar without being one, and its container had nomin-width: 0, so at the panel's floor the tabs spilled past the edge and pushed[+]and the collapse toggle off screen instead of scrolling. This replaces it with@astryxdesign/core/TabListand deletes what the re-implementation carried.Three consequences are not obvious, so they are the reason the shape changed rather than just the markup:
TabrendersendContentinside its own<button>, so a per-tab close would nest a button in a button, andTabListwarns when arole="tablist"strip's direct children are not tabs. Opening and closing therefore share the[+]menu, which lists every registered tool and marks the open ones with a checkmark — the row-end markSelectordocuments as "the house convention shared with Typeahead and CommandPalette", not a boxed checkbox. It carries no shortcut column, because the launcher below already lists every face with its own and that is where a shortcut is learned.TabList hasDividerdraws it on the strip's own box, which ends where the tabs do, so it ran out from under[+]and the collapse toggle and read as a line beneath half a row.Tabdocuments a second idiom for exactly this — a toolbar with a bottom divider, handing the tabs--_tab-indicator-bottomso the selected indicator drops through the bar's gap and lands on the rail — and that is what the bar does now.Replacing the strip also closes the three remaining defects #2188 measured on the old one, which the re-implementation caused and
TabListdoes not:.maka-workbar-tab:hoverand[data-active]shared one rule, so the selected tab's marker was--state-hover-bg, with a second marker (a 2px underline) on top of it. Selection is now weight 600 plusTabList's own rail, and hover is a background — one meaning, one marker, and hovering an unselected tab can no longer make it look selected.[+]28, collapse 32.WorkbarTogglewas hard-wired tomd, which is the titlebar rail's size, correct where it stands beside the sidebar and search actions. It takes asizenow, and the workbar's bar passessm.--space-6plus the OS overlay, borrowed from the window titlebar strip — but that strip measures from the window edge, and this bar starts a content-area gap inside it, so it was paying a gutter it does not stand at. It is--space-2on both sides now, plus only what the platform actually reserves on the right (Windows caption buttons; macOS puts them on the left and reports 0).Four smaller changes ride along because leaving any of them would need a follow-up PR to explain:
SessionTodoPanel, its stylesheet, its shared copy, andWorkbarTodoService, which had no caller left once the hook went. Thetodo:readIPC channel, its preload method andquerySessionTododeliberately stay: that is the boundary Work Board will read through, and it carries its own projection tests.git-branch, which reads as "switch branch". The face lists changed files and their diffs, so it carriesfile-diff.WORKBAR_TOOL_DEFINITIONSbecomes the only place a face's icon, shortcut and order are written. The strip, the[+]menu and the launcher all read it; the surface previously kept two more hand-maintained lists beside it.astryx docs layoutbudgets for a detail panel and the point where five tabs stop fitting (386px needed against 260px available). The 480 default stays — it was set deliberately for the trace face's two-column overview.Refs #2188
Verification
npm run typecheckandnpm testpass (shell-env's login-shell descendant test is flaky on a PID-file race and passes in isolation; it is unrelated to this change).workbar-model.test.tsgains one case for the migration this creates: an install that had the Task face open still carriesworkbar:tasksin v3 storage, and the retired kind must be dropped rather than restored as a tab with no panel behind it. It fails on the parent commit, wheretasksis still a valid kind and the tab is restored.Measured in the built Storybook against
product-session-workbar, since every defect here is a layout one. Each number replaces one #2188 measured on the old strip.The bar's height no longer depends on what is in it — this is the jump the strip used to cause:
[+]Control heights in one row: tab 28 /
[+]28 / collapse 28 (was 28 / 28 / 32).Bar padding: 8px left, 8px right (was 8px / 24px).
The rail, both themes: it spans x 800–1280, the panel's full width, with
[+]at 1208 and the collapse toggle at 1244 standing over it — the old one stopped at 1204, where the tabs ended. The selected tab's indicator spans y 30–32 and the rail y 31–32, so the indicator's bottom edge lands on the rail rather than 1px past it. TheTabListis 28px tall again with no border of its own, so it matches the two buttons beside it instead of overflowing the 32px bar by a pixel.Selected against hovered, both themes, on the new
SeveralFacesstory: the selected tab is weight 600 with no fill; the two unselected are weight 400, and hovering one draws a background it does not share with the selected tab. The old rule gave both the same--state-hover-bg.Overflow at the floor, on
SeveralFacesAtColumnFloor: at a 340px panel the strip needs 284px and has 256px, so it scrolls inside itself (scrolls: true) and[+]stays on screen. This is the 264px-hole defect's real cause — the strip's container had nomin-width: 0, so it spilled instead.The DOM carries no per-tab close buttons, and the stories log no console errors.
SeveralFacesandSeveralFacesAtColumnFloorare new: the story group had no multi-tab scenario at all, so neither the selected/hover distinction nor the overflow behaviour had anywhere to be seen. They open 变更 + 浏览器 + 生成文件 rather than Work Board, because this group's bridge stubs noworkBoardservice and that panel subscribes to it on mount.Screenshots
Each image is one page, BEFORE (
main) on the left and AFTER on the right, shot from two Storybook builds of the same stories at 1280×760, 2× DPR, cropped to the workbar column. To make the left half honest rather than a harness artifact, the parent commit's story wrapper was given the two things the AFTER stories add and nothing else: the--maka-plate-titlebar-clearancethe real AppShell declares (without it the bar floats mid-panel in both builds), and thealsoOpenhelper, since the group had no multi-tab story to compare against.The strip, three faces open — pills with a per-tab
×becomeTabListwith its rail, and the rail runs the whole bar rather than stopping where the tabs do. The bar is 32px in both, so nothing below it moves.The same strip at the panel's floor — before, the third tab is clipped mid-glyph with nothing to say it continues; after, the strip scrolls inside itself and shows the chevron.
What
[+]puts on screen. Before, it swapped the whole panel for the launcher list, so the face you were reading disappeared to open another; after, it opens a menu over the panel, with a row-end checkmark on the faces already open. (The left half is the launcher itself, becauseonOpenLauncheris a no-op in the story harness and cannot be clicked.)The launcher — 待办 is gone and 变更 carries
file-diffinstead ofgit-branch.The remaining faces, unchanged below the bar (变更 / 生成文件 / 追踪 / 侧边对话)
Review focus
[+]is now the only way to close a face. If that reads as a regression against per-tab close, the constraint above is the reason, and the alternative would be droppingrole="tablist"— which is what this issue set out to fix.AI use
Tool(s) and scope: Claude Code — drafted the implementation and the Storybook measurements under my direction and review; the design was settled by comparing rendered alternatives, not generated.
Checklist
Does this PR entail a change in behavior?