Skip to content

Switcher: icon view button, pin in a ⋯ menu, tighter bar - #396

Merged
ZacxDev merged 2 commits into
mainfrom
feat/switcher-compact
Nov 1, 2026
Merged

ZacxDev merged 2 commits into
mainfrom
feat/switcher-compact

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Targets main. It builds on the session switcher on Terminal views (#393, merged as 3cce631).

The operator asked for three changes to the bottom bar: icons instead of each view's text label, the pin moved into a three-dots menu, and less padding so more of the session pills show.

What changed

  • The view button is an icon. Clock = Recent, bell = Waiting, pin = Pins, grid = All. It is a 44×44 target around a 32px face.

    • The server renders all four icons. The inline script toggles hidden before paint, so a stored view shows its own icon with no flash and no shift.
    • Each icon is role=img with an aria-label and a title. The button keeps its Showing <view> sessions; choose a view name and title.
    • The view menu keeps its words and shows each view's icon beside them.
  • The pin moved into a ⋯ menu. The standalone pin button is gone. #switcher-more-btn, at the bar's right end after the search, opens a popover role=menu (#switcher-more).

    • It uses popovertarget, so it opens without script.
    • It is placed above its button and closes once used.
    • Escape closes it and returns focus to the ⋯; Tab also closes it. An arrow key on the button opens it.
    • The pin form is unchanged: same action, fields, view=switcher, back, htmx answer and out-of-band rows. There is still one #switcher-pin per page.
    • The item is role=menuitem with data-pinned. It no longer uses aria-pressed, which ARIA does not allow on menuitem.
    • Pages with no page session get no ⋯.
  • Tighter bar, 44×44 targets kept (operator decision). The view, search and ⋯ buttons stay w-11 h-11. The room comes from three places:

    • the pinned bar has no inset beyond env(safe-area-inset-left/right/bottom);
    • the bar's row has no gaps (each 44px box already sets its 32px face 6px in);
    • pills use px-2/gap-1.

    switcherHeight/switcherRoom are unchanged. Pills still show their whole label, and the row still scrolls sideways.

Measured at 390px (the shown row's clientWidth)

page main before this PR this PR
Terminal view (bar spans the screen) 182px 258px
agent chat page (bar inside its pane) 174px 226px

Tests

  • internal/ui/switcher_compact_test.go has 3 tests:
    • each view's icon and its accessible name;
    • the ⋯ menu holds the pin form, with its action, fields and back;
    • no standalone pin, and no ⋯ when there is no page session.
  • A 390×844 e2e test in composer-switcher.spec.ts covers:
    • the row width (≥ 210; it was 174 before this PR);
    • the icon changing with the view while the button stays put;
    • ⋯ → Pin → this session appears in the Pins view, then Unpin.
  • Red before the change (c0ee49c, whose bar is the same as main's before this PR), green here.
  • pins.spec.ts asserts the Terminal view's row is ≥ 240px.
  • go test ./internal/ui/ ./internal/api/ against a fresh private Postgres 16: 3305 PASS, 0 FAIL, 0 SKIP.
  • go vet ./... and gofmt are clean, and make css-check matches.
  • e2e: 129 passed, 0 failed across 9 specs, then pins and composer-switcher again after the last comment edits (18 passed).
    • Per spec: layout 4, pins 10, switcher-search 10, composer-switcher 8, agents-switcher 18, cls 21, cls-long-transcript 43, terminal 9, terminal-overflow 6.
    • CLS is 0 or 0.00026 everywhere outside the positive controls.

Not verified

  • No e2e test covers the ⋯ menu's keys (an arrow on the button to open it, Escape, Tab).
  • The no-script path (opening the popover, then the form post and its 303 to back) was not run in a browser. A Go test covers the 303.
  • The bar no longer shows whether this session is pinned until you open ⋯. The Pins view still shows it.

🤖 Generated with Claude Code

…e pills show

The operator asked for icons instead of the view button's labels, the pin
moved into a three-dots context menu, and less padding so more session
pills fit.

- View button: icon only (clock = Recent, bell = Waiting, pin = Pins,
  grid = All), 40x44 around a 32px face. Every view's icon is rendered and
  the inline script toggles `hidden` before paint, so a stored view shows
  its icon with no flash and no shift. Each icon is role=img with an
  aria-label and title; the button keeps its "Showing <view> sessions"
  aria-label and title. The view menu shows each view's icon beside its
  words.
- Pin: the standalone button is gone. A ⋯ button (#switcher-more-btn) at
  the bar's right end opens a popover role=menu (#switcher-more) holding
  the same pin form (same action, fields, view=switcher, back, hx-post and
  out-of-band rows; still one #switcher-pin id per page). Its item is a
  role=menuitem with data-pinned in place of aria-pressed, which menuitem
  does not allow. It opens with popovertarget, so it works without script;
  the script adds arrow/Home/End/Escape/Tab, places the menu above the
  button, and closes it on submit. No page session means no ⋯.
- Padding: icon buttons are w-10 (40px) and still h-11, the pinned bar's
  inset is 4px a side (max with env(safe-area-inset-left/right)), row gap
  is gap-1, pills are px-2/gap-1. Bar height and room are unchanged.

Measured at 390px, shown row clientWidth: 182 -> 250px on a Terminal
view's screen-wide bar, 174 -> 226px on an agent chat page.

Tests: switcher_compact_test.go (icon per view with name, pin form in the
⋯ menu, no standalone pin, tight classes) is red at the base branch
(c0ee49c) and green here. A new 390x844 e2e test in composer-switcher
covers the row width, the icon changing with the view, and ⋯ -> Pin ->
the Pins view. It is red at the base (row 174 < 210). Existing Go/e2e
assertions on labels, aria-pressed, 44px widths and class strings were
updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev force-pushed the feat/switcher-compact branch from fc82be9 to fd2caf9 Compare November 1, 2026 20:29
@ZacxDev
ZacxDev changed the base branch from feat/switcher-on-terminal-view to main November 1, 2026 20:29
@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 0 (requirements & deletion): requirement questioned (Q1, forcing: user). Ledger: requirements 9 (unattributed 2) · deletion candidates 3.

D1 — the ⋯ menu's ArrowUp/ArrowDown/Home/End roving-focus handlers (internal/ui/switcher.go:1004-1007): the menu holds one item, so every key focuses the same item; keep Escape/Tab/focus-return
D2 — TestTheBarIsTight (internal/ui/switcher_compact_test.go:298): Tailwind class-name assertions already pinned as whole strings in switcher_test.go and measured behaviourally by the e2e row-width checks
D3 — the stray console.log(`PINNED-ROW-…`) measurement line in e2e/tests/pins.spec.ts:296
Q1 — The bar's icon buttons are now 40px wide (switcherIconButtonClass), below the app-wide 44×44 tap-target rule that e2e/tests/layout.spec.ts enforces. Does the ask to reduce padding on the buttons waive that rule for this bar, or should the 44px hit targets stay and the room come from gaps, the inset or overlapping hit areas? · forcing: user

@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author
Q1=answered: keep 44×44 hit targets on the bar's icon buttons; take the room from the side inset, gaps and pill padding instead

… keys

Following the operator's decision, the bar's view, search and ⋯ buttons go
back to 44px-wide targets (w-11). The extra room now comes from three
places: the pinned bar has no inset beyond the safe-area insets, the bar's
row has no gaps (each 44px box already sets its 32px face 6px in), and the
pills keep px-2/gap-1.

Measured at 390px (the shown row's clientWidth):
- Terminal view: 182px on main before this PR -> 258px
- agent chat page: 174px -> 226px
layout.spec passes.

- The ⋯ menu holds one item, so its ArrowUp/ArrowDown/Home/End handlers
  are gone. Escape, Tab, focus-return, and an arrow on the button to open
  it all remain.
- Dropped TestTheBarIsTight. It only re-checked class strings that other
  tests already pin.
- Reverted the 44->40 test edits and removed the stray measurement logs.
- Comments now paraphrase the operator instead of quoting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 1 (nine-axis checklist): 0 🔴, 0 🟡, 3 🟢. No 🔴, so the ladder ends; the 🟢s are recorded open here rather than fixed in this PR.

1. no-script ⋯ popover opens at viewport (0,0), not above its button (m-0, switcher_pin.go:107)
2. Shift+Tab from the ⋯ item lands on the search, skipping the ⋯ (Tab handler hides first, switcher.go:1009)
3. composer-switcher.spec.ts:361 "drawn before paint" is a retrying attribute check, not a pre-paint measurement

@ZacxDev
ZacxDev marked this pull request as ready for review November 1, 2026 21:41
@ZacxDev
ZacxDev merged commit 14b17bd into main Nov 1, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant