Skip to content

fix(desktop): rebuild the workbar shell on TabList and one open/close control - #4789

Open
Astro-Han wants to merge 1 commit into
mainfrom
fix/desktop-workbar-tab-strip-on-tablist
Open

fix(desktop): rebuild the workbar shell on TabList and one open/close control#4789
Astro-Han wants to merge 1 commit into
mainfrom
fix/desktop-workbar-tab-strip-on-tablist

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

The right workbar drew its own tab strip: a hand-written role="tablist" div of ghost Buttons 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 its 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. This replaces it with @astryxdesign/core/TabList and deletes what the re-implementation carried.

Three consequences are not obvious, so they are the reason the shape changed rather than just the markup:

  • Tabs have no close control. 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 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.
  • Tabs no longer reorder. The strip's order is the order the faces were opened in, and dnd-kit leaves the dependency tree with it.
  • The strip does not set the bar's height, and does not own the rail. The height comes from the titlebar clearance the bar stands in. The rail comes from the bar too: 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 documents 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.

Replacing the strip also closes the three remaining defects #2188 measured on the old one, which the re-implementation caused and TabList does not:

  • Selected and hovered read the same. .maka-workbar-tab:hover and [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 plus TabList's own rail, and hover is a background — one meaning, one marker, and hovering an unselected tab can no longer make it look selected.
  • Adjacent controls were different heights — tab 28, [+] 28, collapse 32. WorkbarToggle was hard-wired to md, which is the titlebar rail's size, correct where it stands beside the sidebar and search actions. It takes a size now, and the workbar's bar passes sm.
  • Asymmetric padding with no reason. The bar's right pad was --space-6 plus 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-2 on 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:

  • 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. The todo:read IPC channel, its preload method and querySessionTodo deliberately stay: that is the boundary Work Board will read through, and it carries its own projection tests.
  • Review carried git-branch, which reads as "switch branch". The face lists changed files and their diffs, so it carries file-diff.
  • WORKBAR_TOOL_DEFINITIONS becomes 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.
  • 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 480 default stays — it was set deliberately for the trace face's two-column overview.

Refs #2188

Verification

npm run typecheck and npm test pass (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.ts gains one case for the migration this creates: an install that had the Task face open still carries workbar:tasks in 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, where tasks is 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:

bar height strip height [+] collapse
no face open 32 y=2, h=28 28
one face open 32 32 y=2, h=28 28
three faces at the 340 floor 32 32 y=2, h=28 28

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. The TabList is 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 SeveralFaces story: 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 no min-width: 0, so it spilled instead.

The DOM carries no per-tab close buttons, and the stories log no console errors.

SeveralFaces and SeveralFacesAtColumnFloor are 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 no workBoard service 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-clearance the real AppShell declares (without it the bar floats mid-panel in both builds), and the alsoOpen helper, since the group had no multi-tab story to compare against.

The strip, three faces open — pills with a per-tab × become TabList with 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.

light dark

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.

light dark

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, because onOpenLauncher is a no-op in the story harness and cannot be clicked.)

light dark

The launcher — 待办 is gone and 变更 carries file-diff instead of git-branch.

light dark
The remaining faces, unchanged below the bar (变更 / 生成文件 / 追踪 / 侧边对话)
light dark
变更
生成文件
追踪
侧边对话

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 dropping role="tablist" — which is what this issue set out to fix.

AI use

  • Generative tooling made a substantive contribution

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

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/XL Under 2500 readable lines label Sep 4, 2026
@Astro-Han
Astro-Han force-pushed the fix/desktop-workbar-tab-strip-on-tablist branch 2 times, most recently from e4f7996 to 6885bdb Compare September 4, 2026 15:08
@Astro-Han
Astro-Han force-pushed the fix/desktop-workbar-tab-strip-on-tablist branch 4 times, most recently from 0387e62 to ef9ab30 Compare September 4, 2026 18:10
… 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
Astro-Han force-pushed the fix/desktop-workbar-tab-strip-on-tablist branch from ef9ab30 to 68881d6 Compare September 4, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Under 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant