Skip to content

feat(switcher): the session switcher on every Terminal view - #393

Merged
ZacxDev merged 2 commits into
mainfrom
feat/switcher-on-terminal-view
Nov 1, 2026
Merged

ZacxDev merged 2 commits into
mainfrom
feat/switcher-on-terminal-view

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

What

The operator asked: "Include the bottom bar in the agent terminal view as well as in the render view where it already is so that I can easily switch agents from the terminal view until we need to go back."

The session switcher (internal/ui/switcher.go) now appears on every Terminal view, at the bottom of the column:

  • a mirrored window's Terminal view: windows with a Rendered view, terminal-only windows, read-only hosts (composer disabled) and offline hosts (no composer);
  • a dispatched claude-code agent's Terminal view (/agents/{name}/terminal).

The bar shows even when the composer is disabled or missing, because switching sessions doesn't depend on being able to send.

How

  • ui.AgentTerminalView.Switcher. When there's a composer, the bar is drawn inside it as its last row, as on the Rendered view, but in flow rather than Pinned. When there's no composer, it goes in a box exactly the bar's height. The safe-area inset moves from the key bar or form to the bar.
  • mirrorComposer: an in-flow composer that holds the bar now gets relative. Before, only its / menu or suggestion row set that, so the absolutely placed bar would have been positioned against the page instead of the space reserved for it.
  • SwitcherCurrent.Back: when you pin without script, the page returns to the Terminal view instead of the session's other view.
  • The header pin that terminal-only windows had (TerminalPin) is removed. The bar's pin replaces it, so a page has only one #switcher-pin. The pin route's answer now always carries the bar's rows out of band.
  • Opening an agent's Terminal view now records a recent visit, like the mirrored Terminal view and the chat page already do. It is recorded before the switcher is built, so Recent leaves out the page's own session.

Terminal size (hazard 1)

The page only ever sends a size that covers the far side's window, and termsize.Decide on the far side never shrinks a window. So the bar can't shrink the remote tmux window in any case.

To avoid resize churn, the bar is server-rendered at a fixed height and appears in the document before the terminal's script runs. The terminal's first proposed() measurement therefore already has the bar's space taken. The pending-until-whole visibility and switching views in the bar change no layout, so no second resize is sent.

Side effect on desktop: if this browser had previously grown the window to the full page height, the page is now about 3 rows shorter than the window. It shows that window in its scroll area instead of resizing it. Round 1 (Y1): that scroll area now starts at its bottom and goes back there on every redraw, so the prompt and status line (a TUI's last rows) stay on screen. It stays put if the person scrolled away, until they scroll back to the bottom.

The bar steps aside (round 1, Y2, operator decision)

  • Keyboard: on the Terminal views and the Rendered view, the bar hides and gives its room back while a text field has focus and the visual viewport is at least 150px shorter than the tallest it has been at this width (<html data-keyboard>). It comes back when the keyboard closes. 150px is smaller than any phone keyboard (≈200–350px) and larger than a browser toolbar collapsing (≈50–100px), so the toolbar doesn't make the bar flicker.
  • Very short screen: a CSS media query hides it at max-height: 30rem, so the decision is made before the first paint. This applies to the Terminal views only. The Rendered view's pinned composer reserves a measured height whose first-paint fallback includes the bar, so collapsing the bar with a query there would shift the transcript on load. 30rem (480px) is taller than any landscape phone and shorter than the smallest portrait phone's page.
  • The agent chat page's bar is unchanged.

Tests

  • Red/green: TestTheMirroredTerminalViewsDrawTheSessionSwitcher (3 cases) and TestAnAgentsTerminalViewDrawsTheSessionSwitcher fail at origin/main b60d5d9 (0 #session-switcher) and pass here.
  • TestATerminalViewsSwitcherIsTheColumnsFootAndInPlaceBeforeTheTerminalMeasures checks that the bar comes before the terminal's script, sits in its reserved space with relative, and carries the safe-area inset alone. I broke each of the first two on purpose and saw the test fail with that check's own message.
  • e2e at 390×844, for both a mirrored and an agent Terminal view: the bar is visible with the page's session marked current, CLS stays under 0.01, the fake far side received exactly one size, switching views sends no resize, the input stays above the bar with a soft keyboard open, and tapping an entry opens that session. Both tests fail at origin/main.
  • pins.spec: a shell window's pin is now found in its bar.

Known limitation

When the Terminal view is opened in the view-swap frame over the Rendered view, pinning in one document doesn't refresh the other document's bar until it reloads.

🤖 Generated with Claude Code

@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 0 (requirements & deletion): proceed to the checklist. Ledger: requirements 9 (unattributed 3) · deletion candidates 2 · no open premises.

D1 — the agent Terminal view's new recordAgentVisit (internal/api/terminal.go:314): no test covers it, and its comment's ordering reason is false (NewSwitcher already leaves the page's own session out of Recent); keep it for the go-back-via-Recent reason and fix the comment, or drop it
D2 — SwitcherCurrent.Back (internal/ui/switcher.go:125, :262): read only by the no-script pin fallback; keeps the old header pin's return to the Terminal view; cheap to keep, safe to delete

@ZacxDev

ZacxDev commented Nov 1, 2026

Copy link
Copy Markdown
Owner Author

Round 1 (nine-axis checklist): 0 🔴, 3 🟡, 4 🟢. No 🔴, so the ladder ends; the 🟡s are fixed un-re-audited or recorded open.

1. (🟡) desktop overflow after the bar shortens the terminal hides the bottom rows (the Claude Code prompt) with the scroll area at the top — pin overflow scroll to the bottom or record the limitation
2. (🟡) the fixed-height bar takes ~1/3 of the terminal on short viewports (landscape / keyboard open) — collapse or hide it, or record it
3. (🟡) add a Go test: a view=switcher pin with a Terminal `back` answers with the hx-swap-oob rows
4. (🟢) terminal-only window loses its pin when the switcher list fails to build
5. (🟢) the term-keys order check in terminal_switcher_test.go passes vacuously when #term-keys is missing

ZacxDev added a commit that referenced this pull request Nov 1, 2026
go test's default timeout is 10 minutes per package. Under -race -cover,
internal/api already takes 460-575 s on main's runs, and on a runner that
was slower across every package (agentprovision 123 s vs 91 s) it passed
600 s and panicked mid-suite with nothing hung — three times on PR #393,
and once on another PR before it. Locally that PR's api package was 362 s
against main's 345 s, all tests passing, so the time was the runner's.

A real hang still fails, at 20 minutes instead of 10. The pin in
internal/modulegate/ci_verdict_steps_test.go is updated to the new line
(watched red with the old pin against the new step).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ZacxDev and others added 2 commits November 1, 2026 13:32
The operator asked for the bottom bar on the Terminal view too, so a session
can be switched from there without going back to the Rendered view. Every
Terminal view now draws it as the column's last row: a mirrored window's
(with or without a Rendered view; composer enabled, disabled or absent) and
a dispatched claude-code agent's.

- ui.AgentTerminalView.Switcher: drawn inside the composer as its last row
  (as on the Rendered view, but in flow, not Pinned), or in a box of exactly
  its room when there is no composer. The safe-area inset moves to the bar.
- mirrorComposer: an in-flow composer holding the bar is now its containing
  block (relative); before, only its / menu or suggestion row made it so, and
  the absolute bar would have been placed against the page.
- The bar is server-rendered before the terminal's script, at a fixed height,
  so the terminal's first measure already has the bar's room taken: no
  second, smaller resize, and a view switch in the bar sends none.
- SwitcherCurrent.Back: the pin toggle's no-script return is the Terminal
  view itself. The header pin a terminal-only window had (TerminalPin) is
  gone: the bar's pin is that page's pin now, and the pin answer always
  carries the bar's rows out of band.
- The agent Terminal view records a visit, as the mirrored one and the chat
  page do.

Tests: handler tests that every Terminal view draws #session-switcher with
its own session current (red at origin/main, green here); a ui test that the
bar precedes the terminal's script and sits in its reserved room
(mutation-checked); e2e at 390x844 for a mirrored and an agent Terminal view
(bar, current, CLS, one size heard by the far side, a tap switches); the
pins spec now finds a shell window's pin in its bar.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… aside for the keyboard

Y1: a session taller than the page (a pod window grown once to a full desktop
height, which termsize never shrinks) was drawn in overflow at scrollTop 0,
hiding its prompt and status line on every load. The overflow view now
starts at its bottom and returns there on every redraw (a relayout, Fit
width, the switcher stepping aside and back), unless the person scrolled it
away; entering overflow afresh starts at the bottom again. The page's own
scrolls (the sizer collapsing in proposed(), the pin) are not taken for the
person's.

Y2 (operator decision): the session switcher at a screen's foot steps aside,
its room given back, while a soft keyboard is up — a text field focused and
the visual viewport >= 150px shorter than its tallest at this width
(<html data-keyboard>) — on the Terminal views and the Rendered view; and on
a screen <= 30rem tall (a landscape phone) by a CSS media query, on the
Terminal views only (the Rendered view's pinned composer reserves a measured
height whose first-paint fallback counts the bar). The agent chat page's bar
is unchanged.

Y3: a Go test that a view=switcher pin whose back is a Terminal view answers
with the bar's rows out of band. G5: the bar-below-keys check asserts
#term-keys exists. D1: the recordAgentVisit comment gives the real reason,
and the recents ledger covers the agent Terminal view's visit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev force-pushed the feat/switcher-on-terminal-view branch from 4d0b9ab to c0ee49c Compare November 1, 2026 19:32
@ZacxDev
ZacxDev marked this pull request as ready for review November 1, 2026 20:00
@ZacxDev
ZacxDev merged commit 3cce631 into main Nov 1, 2026
19 checks passed
@ZacxDev
ZacxDev deleted the feat/switcher-on-terminal-view branch November 1, 2026 21:41
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