Repository navigation
feat(appearance): add a phone-only terminal scrollbar toggle - #945
ThinMaPros wants to merge 2 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Summary
Merge Risk: ⚪ Minimal · up to
|
…me layout reads (#955) ## Problem `TerminalViewportTools` (the native-history scrollbar and find-highlight overlay) keeps a visible, idle pane asking herdr for its scroll metrics once a second (`GET /api/pane/scroll`, one `paneScrollInfo` RPC each) for as long as the pane stays open. On a phone over cellular or Tailscale this keeps the radio awake. It also runs `paint()` on every xterm `onRender`. `paint()` calls `getClientRects`, `getComputedStyle` and two `getBoundingClientRect`s right after the DOM renderer changed the rows, so each frame forces a synchronous layout during heavy output, even when no search is active. ## Cause - `read()`'s `finally` always re-armed `schedule(dirty ? 250 : 1000)`, so with no output at all it still polled every second. - `paint()` measured both the box geometry and the find rectangles, and was bound to `terminal.onRender`. The box only moves on a resize or a mount scroll, and the ResizeObserver, `onResize` and the scroll listener already handled those. ## Fix - New pure helper `idleScrollPollDelay(idleReads)` in `src/lib/terminalViewport.ts`. Reads in a row with nothing between them wait 1 s, 2 s, 4 s, 8 s, then 15 s (the cap). While output is arriving, polling stays at 250 ms (`SCROLL_POLL_BUSY_MS`). - The count resets on `wake()` (output, a resize, the end of a scrollbar write, the tab becoming visible), on an explicit refresh (a search result), and while the pane is not showing. A scroll made with no output (another client, or herdr itself) is still picked up within 15 s. - `paint()` is split: - `paintBox()` measures the geometry. It runs on resize, on mount scroll, after each metrics read, and on refresh. - `paintHits()` runs on `onRender`. It returns before any layout read unless a search has a result to place. - The ResizeObserver now also watches `.xterm-screen`, so a change in cell metrics (a font load, a zoom) still updates the box now that rendered frames no longer do. ## Tests - `src/lib/terminalViewport.test.ts`: new `scroll metrics poll` cases for the backoff schedule, the cap, the reset and nonsense inputs, and the busy cadence being shorter than the first idle step. - `bun run typecheck`: pass. - `bun run test:unit`: 3072 pass, 0 fail. - `bun run check run --build bun scripts/terminal-viewport-regression.ts`: PASS (scrollbar, search highlights, wide cells, non-jumping refresh, old bridge). - `bun run check run --build bun scripts/application-paging-regression.ts`: PASS (this includes the held page on stale metrics after a program quits, which relies on the read that follows output). - `scripts/terminal-copy-regression.ts`: started under `check run` but printed no result, so it was not actually exercised here. ## Notes - Source: mobile-terminal stability audit, finding render-scroll-3 (low severity: battery, radio and herdr RPC load, plus per-frame layout jank; nothing desyncs). - Overlap: #945 (phone scrollbar toggle) also edits `TerminalViewportTools.tsx` (a `showScrollbar` prop and the render condition) and `src/lib/terminalViewport.ts` (a new constant at the top). The hunks are separate from this PR's, but whichever merges second may need a small rebase. Neither PR duplicates the other. - Left out: - Pushing `offset_from_bottom` / `max_offset_from_bottom` on the attach stream, which would replace the poll entirely. That is a protocol change for a separate PR. - A slower busy cadence on coarse pointers. ### Review follow-up (3b53d5b) - Scrolled-up view: confirmed in the test herdr that a view scrolled up into history is held still while output grows the history (offset and max both grew 155 -> 165) and no bytes reach the client, so with the backoff alone the scrollbar stayed stale for 7.3 s. The poll now keeps its 1 s read while `offset_from_bottom > 0` (`scrollPollDelay`), and a read that returns moved metrics resets the backoff. - Wiring test: `scripts/terminal-viewport-regression.ts` now counts `GET /api/pane/scroll`: a quiet pane at the bottom reads at most 4 times in 8 s (a fixed 1 s poll read 7 and fails), output brings the next read back within 1.5 s, a scrolled-up view reads at least 4 times in 6 s (the backoff alone read 3 and fails), and history grown under it reaches `aria-valuemax` within 3 s. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Merge-session review Fixed (a8c2a68): - A failed metrics read (for example a 502 or timeout on `GET /api/pane/scroll`) hid the scrollbar, and the retry followed the idle backoff, up to 15 s on a quiet pane. The failure path now resets the backoff, so the next read comes after 1 s as before this PR. While no metrics are held, a page key (Mod+Shift+PageUp/Down or the key-bar button) now asks for a fresh read right away. `scripts/terminal-viewport-regression.ts` fails one backed-off read and checks that the retry comes within about 1.5 s and the scrollbar is back within 2 s. It fails without the fix. Deferred: none. A server that stays down is retried every 1 s, the same as before this PR. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Problem On a phone in the terminal lens (line mode), the input line grows from one row up to four as a message wraps or gets line breaks. Each new row took one line of height from the grid. After about 120 ms the grid's ResizeObserver fit one row fewer and sent a pty resize, so a three-line message caused 2-3 resizes while typing and one more when the line collapsed after Send. Each resize makes herdr reflow the pane and the program in it (Claude Code) repaint, and any desktop sharing the pane sees its grid shift. A note under the line (`.terminal-input-note`) and the masked input (SecretInput) taking the line's place changed the height in the same way. ## Cause `TerminalInput` was an in-flow sibling of `.terminal-surface` in the `.terminal-stack` flex column. The grid host (`.pane-terminal`, `flex: 1 1 auto`) got whatever height was left. The ResizeObserver in `PaneTerminal.tsx` (fit, then `socket.resize`) cannot tell a change in the input chrome from a real viewport change. ## Fix - `PaneTerminal.tsx`: in the terminal lens, the input line and the masked input render inside a `.terminal-input-slot`. The slot keeps exactly one row of the input line in flow. The masked input renders from the same slot in the chat lens too, where the slot is box-less (`display: contents`) and the card stays in flow as before. One tree position in both lenses means a lens switch does not remount it, so a typed value or a send in flight survives the switch. - `PaneTerminal.tsx` also measures the room from the slot's bottom to the grid's top (ResizeObserver on the stack and `.terminal-surface`) into `--terminal-input-room`. The grown line (`max-height`, textarea `max-height: 100%` of its row) and the masked input (`max-height`, `overflow-y: auto`) stop there and scroll. In a short pane they are no longer clipped by the pane frame or drawn over the find bar. - `TerminalInput.css`: the slot's height is computed from the same tokens as a one-row line (top border and padding, then the taller of the send button and a one-row textarea). `.terminal-input` hangs from the slot's bottom edge (`position: absolute; bottom: 0`, `z-index: var(--z-banner)`), so extra rows and the note grow upward over the grid's last rows. On a pane where you type straight into the grid (`.is-empty`), no row is reserved. - `SecretInput.css`: inside the slot, the masked input floats the same way. - The CSS uses tokens only. There are no new strings and no protocol change. ## Tests - `scripts/terminal-input-regression.ts` has a new block. It types three lines (Shift+Enter) into the input line, sends, and acknowledges. It then asserts: - the `.pane-terminal` height is unchanged after the line grows and after it collapses; - an in-page ResizeObserver on the grid never fired; - no `resize` frame was sent; - at one row, the line covers no grid row (the line's height equals the slot's). - Fails without the fix: `a growing input line keeps the grid's height` (621 !== 669). Passes with it. - Added after review, in the same script: a refused send (`agent_blocked`) shows the note and then clears it, and the grid height and observer stay unchanged; the one-row parity is checked again under `data-density="compact"`; in a 160px-high viewport a four-line line stays under the grid's top and its text scrolls. The last check fails on the previous commit (`37 < 80`). - `scripts/secret-input-regression.ts`: the field keeps `fixture-secret` across the switch to the chat lens (fails on the previous commit). A desktop context (no key bar) then brings up the masked prompt in the terminal lens with direct typing and with the input line, and asserts the grid height and observer stay unchanged. This part was not run against main without the fix. - `bun run check run --build bun scripts/terminal-input-regression.ts`: pass (run several times). After review, `checkTerminalInput` was run through a small driver under `bun run check run`: pass. - `bun run check run --build bun scripts/secret-input-regression.ts`: pass. A screenshot of the masked input over the grid in the terminal lens was checked by eye. - `bun run typecheck`: pass. `bun run test:unit`: 3069 pass, 0 fail, 8 skip. - Not run: `ui-regression.ts` (the touch input-line block), `check full`/`concurrent`. ## Notes - The overlay covers the grid's last rows while the line has more than one row, or while a note or the masked input is shown. That is the design: the grid underneath keeps its size, and the rows come back when the line collapses. No scroll hint or bottom inset was added for the covered cursor row. Either one would be a policy change of its own. - Out of scope: - The KeyBar still unmounts while the masked input is shown, which changes the host height on a phone. - The soft keyboard opening and closing is still a resize. That is a separate policy question (audit item 2-10, #712). - If the masked input is pending but the socket is down, the reserved row stays empty instead of disappearing. - Overlap: - #936 (dictation backend) edits `TerminalInput.tsx`/`.css`, but not the layout this changes. Expect a small textual merge in the CSS header. - #945 and #951 touch `PaneTerminal.css` only, which this PR does not edit. - Sibling stability PRs from the same audit also touch `PaneTerminal.tsx`/`TerminalInput.tsx` (key bar while composing, reclaiming the pty size from phone inputs). - Source: mobile-terminal stability audit, finding viewport-resize-3 (report 2-6). 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Merge-session review Fixed (908110d): - Low: in a short pane a line grown to its cap (or a tall masked input) painted over the top-right banners, since both sat at `--z-banner` and the slot comes later in the DOM; the banners' buttons (held input Send/Discard, Dismiss) lost their taps and "reconnecting" was hidden. `.terminal-banners` now sits at `calc(var(--z-banner) + 1)`; the column keeps `pointer-events: none`, so the line still takes taps outside the chips. `scripts/terminal-input-regression.ts` raises a banner over the capped line in the 160px-tall pane and checks with `elementFromPoint` that its button is on top (it fails without the fix). Deferred: none. Checks: `bun run check fast` passed; `CI=1 bun run check run --build bun scripts/ui-regression.ts` passed. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#963) ## Problem On a phone, a slow drag scrolls herdr's history steadily, but a quick swipe over the same distance moves only a few notches. The text does not follow the finger, and flicking through a long agent transcript takes many repeated drags. ## Cause `onTouchMove` in `PaneTerminal.tsx` dispatched one synthetic pixel-mode `WheelEvent` per touchmove. With mouse reporting on (herdr attach), xterm 5.5 sends exactly one wheel report per wheel event: `getLinesScrolled` is used only as a zero check, and its `_wheelPartialScroll %= 1` drops every whole row beyond the first. The wheel-speed replay only covers trusted events, so touch got no compensation. A 40-60 px touchmove (a flick) crossed 2-3 rows but sent one report; a slow drag sent about one per row. ## Fix - New pure helper `src/lib/wheelSteps.ts`: `wheelSteps(pixels, stepPixels)` returns the whole signed steps and the pixels left over. `HERDR_MOUSE_SCROLL_LINES = 3` is herdr's default `ui.mouse_scroll_lines`. - The touch handler gathers the gesture's travel (`touchPixels`, reset on touchstart), divides it by cell height x 3, and dispatches that many `DOM_DELTA_LINE` wheels of `deltaY` +-1 (each makes xterm send exactly one report), keeping the rest for the next move. One report then moves herdr about as far as the finger moved. - The selecting drag's wheel (`dragWheel`) uses the same helper and the same `cellHeight()` so the two conversions cannot drift. - `src/AGENTS.md` geometry row updated. No momentum in this PR: a flick still stops where the finger lifts (`onTouchEnd` sends at most one leftover step, see the merge-session review below). render-scroll-2 also proposed a decaying momentum phase; that is deferred to a follow-up and this PR closes only the per-touchmove report loss. ## Tests - `scripts/terminal-pinch-regression.ts` (browser, runs in `ui-contexts-regression`): at the 13 px baseline it drags 300 px once in 2 touchmoves and once in 30, and asserts both send the same wheel count (about 300 / (3 x cell height)) and that every wheel is `DOM_DELTA_LINE`. Verified to fail on main's `PaneTerminal.tsx` ("a 2-move swipe sends line-mode wheels only"). The finger left after a partial pinch release now moves three wheel steps of the 20 px cell (it moved 60 px, under one step, so that negative check could not fail). - `src/lib/wheelSteps.test.ts` (new): five 60 px moves send the same report count as thirty 10 px moves (and 300 x 1 px equals one 300 px move); sign and remainder carry; a reversal pays back gathered travel first; a zero/NaN cell height makes no steps. - `bun run typecheck`: pass. `bun run test:unit`: 0 fail. - `bun run build`, then `bun run check run bun scripts/ui-contexts-regression.ts`: pass, including `checkTerminalPinch` (earlier commit); after the test update, `bun run check run bun scripts/terminal-pinch-regression.ts`: pass. Its one-finger drag went from 60 px to 150 px: at a 20 px font, 60 px is less than one three-row step, so it would no longer send a wheel. That script only asserts that synthetic wheel events are dispatched; it does not check how far herdr scrolls. - `scripts/windows-lens-browser-qa.ts` (manual QA, not a CI lane) is the only script that drags a finger into real herdr history. It failed twice at an earlier step ("the command's output reaches the mirrored terminal", in the mirror-only part), before the touch-scroll step ran. So the herdr-side scroll distance was **not** verified end to end here. ## Notes - The browser cannot read the PC's herdr config. On a PC where `mouse_scroll_lines` was changed, one step scrolls that many lines instead of 3, so the text moves proportionally faster or slower than the finger. - `terminalWheelSpeed` still does not apply to touch (unchanged: the replay is for trusted wheels only). - A slow drag now sends about a third of the reports it sent before. Before, it scrolled about 3 lines per row of finger travel; now it scrolls about 1. - Overlap: #945 (phone terminal scrollbar) does not touch the drag translation. #946 and #951 edit `PaneTerminal.tsx` elsewhere: textual merge-conflict risk only, no semantic overlap. - Source: mobile-terminal stability audit, finding render-scroll-2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Merge-session review Fixed (b462b57): - Short drags no longer get lost. Before this, a one-finger flick shorter than one step (3 rows, so 51 px at the default 13 px font) sent no wheel at all, and every longer drag dropped its last partial step. Now, on `touchend` of a one-finger drag (not a pinch, not an adopted grid, not a `touchcancel`), one more line-mode wheel is sent if at least half a step is left (`finalStep` in `src/lib/wheelSteps.ts`). Unit cases cover `finalStep`. `scripts/terminal-pinch-regression.ts` now checks that a 0.7-step flick sends exactly one wheel and a 0.3-step flick sends none. On the previous head this case failed: "a flick of 0.7 step (40 px) sends one wheel". The changelog fragment and the `src/AGENTS.md` row now describe the touchend step. Deferred: - Momentum/inertia scrolling after a fling (already listed above). This is still out of scope. Checks: `bun run check fast` passes, and `CI=1 bun run check run --build bun scripts/terminal-pinch-regression.ts` passes. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…bar (#964) ## Problem A phone shows a pane in the terminal lens while a desktop tab of the same pane takes the pty back to its own size (a click or key in the desktop terminal, or the desktop window coming back to the front). The phone's xterm stays at phone width while herdr streams rows drawn for the desktop's width, so lines wrap in the wrong place and agent TUIs look scrambled. Typing in the input line, Send, Enter and key-bar taps did not fix it. Only a touch on the grid itself, a focus change or a reload did. ## Cause In the terminal lens, an interact client stores `pane-geometry` in `sharedGridRef` and then ignores it, because it drives the grid itself. The only thing that took the size back while the window kept its focus was `reclaim`, and that runs only for a pointerdown or keydown whose target is inside `term.element`. The phone's usual input paths are all outside that element: - `sendTerminalLine` submits from the `TerminalInput` textarea. - `pressEnter` sends `\r`/keys from the same line. - `pressKey` calls `term.input()` from the key bar, with no DOM event on the grid. ## Fix - The size check moves out of `reclaim` into `reassertIfStale()`. When the shared grid differs from this grid, the tab is in use and has not released its pane, it calls `refit()` (force resize). - `reclaim` keeps its event filters (target inside the grid, no IME/repeat key, primary button) and then calls the helper. - `sendTerminalLine`, `pressEnter` and `pressKey` call it before they send, so the resize frame goes out before the input on the same socket. - `TerminalInput` gets an optional `onFocus`, wired to the same helper. Tapping into the input line counts as the user being back, the same way a tap on the grid does. - No resize was added for the keyboard opening or closing. That is a separate policy question (viewport-resize-2). ## Tests - `scripts/chat-size-regression.ts` `checkFocusedWindowReclaimsTerminalSize` (already in `ui-contexts-checks.ts`, so it runs in the browser lane) now continues with two clients after its existing steps. A desktop at 1280x800 and a phone at 390x844 in line mode take turns: - a tap into the input line; - a line sent with Enter; - a key-bar Ctrl+C; - Enter on the empty line. Each phone step comes after a desktop key has taken the pty back. Each step asserts that the phone's own-size `resize` frame goes out before its `submit`/`input` frame, and that the pane's shell (`stty size`) reports the phone's size again. - `CHAT_SIZE_CASE=reclaim bun scripts/chat-size-regression.ts` now runs that check on its own. - Without the `src/` change the new step fails ("a tap into the input line takes the pane back to the phone's grid"). With it, `bun run check run --build bun scripts/chat-size-regression.ts` (with `CHAT_SIZE_CASE=reclaim`) passes. - `bun run typecheck` and `bun run test:unit` (3069 pass, 0 fail) pass. - `check full` / `concurrent` was not run. ## Notes - Source: a mobile-terminal stability audit, finding viewport-resize-1. Related: #472 (the chat-lens version) and #712 (an attach holds the pane's size). Neither covers this path. - No open PR touches `reclaim`, `sharedGridRef`, `sendTerminalLine`, `pressEnter` or `pressKey`. #951, #950, #946, #945, #936, #935, #932, #910, #894, #859 and #822 edit other parts of `PaneTerminal.tsx`. #936 also edits `TerminalInput.tsx` (dictation), so a small textual conflict is possible there. - The size tug-of-war between two devices that both type into the same pane remains by design: whichever device the user is typing on takes the pty. A "sized by another device" notice and passive adoption of the foreign grid were left out. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
Follow-up: we looked at this again. Rather than a toggle, we are removing the terminal history scrollbar everywhere in #1113, since the wheel, phone drag, page keys and Find already scroll the shared history (Paseo and Moshi show no terminal scrollbar either). Your point about the column it costs on an iPhone is what started this. Thank you, @ThinMaPros. |
## Why Owner decision (2026-10-15): delete the terminal history scrollbar outright, with no setting. Paseo and Moshi show no terminal scrollbar either, and on phones the bar reserved a 24px column beside the grid. History stays reachable: the wheel, a finger drag, Mod+Shift+PageUp/PageDown and Find all still scroll herdr's shared history. ## Removed - The `terminal-history-scrollbar` track and thumb in `TerminalViewportTools.tsx`, with its drag, click and keyboard (arrows, Page Up/Down, Home/End) handlers, its preview state, the `viewportId` prop, and the `scrollThumb`/`offsetAtThumb` helpers (and their unit test). - The reserved column: the `.pane-terminal.has-viewport-tools` width rule, the `--terminal-history-gutter` variable, and the single-pane header offsets in `PaneCanvas.css`. The grid now fits the full mount width (the fit addon reads the mount; no cols math subtracted the column). - Its CSS, the `Terminal scrollback` i18n entry (ko/ja/zh), and the scrollbar passages in DESIGN.md, docs/guide.md and the demo log line. ## Kept - The scroll metrics poll (`GET /api/pane/scroll`) the page keys rely on, Mod+Shift+PageUp/PageDown paging with pending-page accumulation, find highlights and jumps (and Find cancelling queued pages), phone drag and wheel scrolling. No `server/` or `shared/` changes; `pane.scroll` and `/api/pane/scroll` are untouched. ## Tests `scripts/terminal-viewport-regression.ts` now asserts that no scrollbar is drawn and that the grid spans the whole terminal surface. Steps that used the scrollbar now page with the shortcut. The paging, rebound-shortcut, poll back-off, Find ordering and old-bridge checks are kept. - `bun run typecheck`: exit 0 (also after merging origin/main) - `bun run test:unit`: exit 0 (3645 pass, 0 fail, 8 skip) - `bun run build`: exit 0 - `bun run check run sh -c "bun scripts/terminal-viewport-regression.ts && bun scripts/terminal-pinch-regression.ts && bun scripts/ui-regression.ts"`: exit 0 (`terminal-pinch-regression` covers one-finger drag scrolling) Supersedes the idea in #945.
…ory (#1036) <!-- See CONTRIBUTING.md for checks, translations, contract tests and the changelog (a file in changelog.d/, never CHANGELOG.md). --> ## Change Closes #1034 In the Terminal lens, scrolling herdr's shared history up leaves the pane with no one-tap way back to the live bottom: the history scrollbar's End key is the only shortcut, and it is out of reach on a phone. Chat already has a round "Jump to latest" button for this. While herdr reports `offset_from_bottom > 0` and the pane is interactive, a round ArrowDown "Jump to bottom" button now floats over the grid's bottom-right corner, left of the history gutter. Tapping it sends the same `pane.scroll` offset 0 as the scrollbar's End key, through the existing coalesced `writeRef` path in `TerminalViewportTools`. A failed write falls back to the existing `Scroll failed` error. The button: - is hidden at the bottom, in the chat lens, in observe and read-only views, and while Find owns the viewport (it renders only when `enabled && interactive`); - keeps focus in the terminal (and the soft keyboard up) via `onPointerDown` `preventDefault`, the same pattern as the input row's buttons; - reuses the Chat button's round form with tokens only (`--control-h`, `--touch-target` on coarse pointers, `--radius-pill`, `--shadow-pop`, `--icon-size`), no color literals, no new animation; - is anchored to the grid's bottom-right corner clipped to the mount's client area, so an adopted grid (a mirrored PC, an observer) that is wider than the mount keeps the button inside the overlay instead of under the gutter. No new state, polling, endpoint, WS frame or protocol type; `site/demo/transport.ts` already answers `pane/scroll`. New string `Jump to bottom` is translated in ko, ja, zh and tr. `DESIGN.md` (terminal history section) and a `changelog.d` fragment are updated. Heads-up: #945 (phone scrollbar toggle) also edits `TerminalViewportTools.tsx` and `DESIGN.md`. There is no functional dependency, only a possible textual conflict. ## Validation - `bun run check fast`: generated types fresh, typecheck, build and unit tests (3470 pass, 0 fail, includes `i18n.test.ts`, `conventions.test.ts`, `motion.test.ts`). - `scripts/terminal-viewport-regression.ts` gains a step: no button at the native bottom, button visible after Home and left of the gutter, with the terminal textarea focused a click returns herdr to offset 0, the button detaches, focus stays on the textarea and no input frame is sent. - On my macOS machine (herdr 0.9.3) the script already stops at the existing step "repeat paging accumulates against the pending preview", identically on untouched `main`, so I could not run it end to end here. With that one block removed in a throwaway copy the whole script, including the new step, prints `PASS native history scrollbar, ...`. Please rely on the CI browser lane for the full script. - One-off Playwright check (not committed): 390x844 viewport with a coarse pointer, on an attached pane and on a mirror pane (`createServer({ terminalAttach: false })`, adopted grid). In both the button is 40x40, inside the mount's client area and left of the gutter. - Not verified: a real iOS or Android soft keyboard (only Playwright mouse and touch emulation), and an adopted grid that is actually wider than the mount (covered by code reading and the clip logic only). Co-authored-by: Hako <devswha@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Change
On an iPhone, the terminal's shared-history scrollbar reserves a 24px column. There is currently no way to hide it and recover that space.
Add Settings → Appearance → Show terminal scrollbar:
(max-width: 768px) and (pointer: coarse)). A narrow desktop window does not expose the control.This PR has two commits. The first fixes a pre-existing mobile search bug uncovered by the new regression:
FindBarand the terminal input line reused the same sibling React key, causing duplicate search forms to consume the terminal's height. The second adds the scrollbar preference and the combined regression coverage. The key fix is needed to verify that mobile search still works with the history bar hidden.This does not change the swipe algorithm or add momentum scrolling.
Validation
On final commit
6017eb9341e784ac601d007b14a2a627af77fd34(macOS arm64, Bun 1.4.2, herdr 0.9.3):TMPDIR=/private/tmp bun run check fast: passed generated-type freshness, typecheck, build and the unit suite. Workflow syntax is skipped locally because actionlint is pinned to Linux x64; CI checks it.TMPDIR=/private/tmp CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" bun run check run --build bun scripts/terminal-viewport-regression.ts: passed against an isolated herdr with temporary config/state, not the user's running panes.git diff --check: clean.For both final check runs, a temporary process-local Git excludes file ignored only
.DS_Storeso Finder metadata changes could not invalidate the source fingerprint. No ignore file, generated build output or macOS metadata is included in this PR. Both final reports recorded unchanged source and exit 0.Full integration/browser lanes and independent maintainer review remain for CI/review.