With five tabs open on a phone, the right-hand tabs were effectively
unreachable. Selecting a tab only toggled the .active class, so the strip
never moved, and every full rebuild (a task badge appearing, a session
created elsewhere) replaced the strip's innerHTML, which resets scrollLeft
to 0 and yanked a mid-swipe strip back to the first tab.
Three changes, which only work together:
* computeTabScrollLeft() (pure, constants.js) decides the scroll target from
measured rects, and _scrollActiveTabIntoView() applies it on selection.
Rect math on the strip's own scrollLeft rather than scrollIntoView(), which
also scrolls ancestors: on a phone that is the document, under a fixed
header and possibly an open keyboard.
* _fullRenderSessionTabs() saves and restores scrollLeft across the rebuild,
and re-reveals the active tab only when it actually changed
(_lastRenderedActiveTabId), so a background render never undoes a manual
swipe.
* Mobile no longer hoists the active session to the front of the strip. That
reordering ran on full renders only, so tab order flipped depending on
which render path fired, and it renumbered the Alt+N badges. Scrolling the
active tab into view replaces it.
Also sets overscroll-behavior-x: contain on the strip so a swipe that runs
past the last tab stays in the strip instead of becoming the browser's back
gesture.
Tests: scroll-target math in test/tab-overflow.test.ts (runs in CI), plus
five browser regressions in test/mobile/tabs.test.ts covering reveal-on-
select in both directions, scroll preservation across an ambient rebuild,
sessionOrder rendering on phones, and a real touch drag reaching the last
tab.
Closes#257
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing the dead `mobile-collapsed` tray (this PR) means the test that
asserted the headerRight tray *stays collapsed* on mobile now contradicts
the code and would fail when run. Flip it: with the three-dot utility
toggle gone, the header-right utilities must flow inline and stay
reachable on small viewports. The response-viewer eye itself remains
hidden by default (showResponseViewer opt-in), so this only re-exposes
the already-default-visible utilities inline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-resume on usage limit ("token pause" control, opt-in checkbox at the
top of the Respawn tab, off by default):
- usage-limit-patterns.ts (new, pure): detects all Claude Code limit
messages (1.0.x-2.1.x eras incl. "5-hour limit reached - resets 8pm",
"You've hit your limit - resets 1:40pm (TZ)", weekly date forms, raw
"usage limit reached|<epoch>") and parses the reset time. Conservative:
no parseable future reset time, no action.
- SessionAutoOps: arms a timer at reset+2min, sends Esc (dismisses the
rate-limit dialog) + "continue"; dedups footer redraws, retries every
5min on stale times, cancels when Claude starts working, persists and
re-arms across Codeman restarts (SessionState.autoResumeEnabled/At).
- Respawn guard: cycles are blocked while limit-paused so /clear cannot
wipe the paused conversation (respawnBlocked reason 'usage_limit').
- POST /api/sessions/:id/auto-resume; SSE session:limitPauseScheduled/
limitResume/limitResumeCancelled; toasts + status line in the modal.
- Respawn tab tidied: single-row prompt fields, merged behavior row.
Mobile fixes (0.9.8 regressions, user-reported):
- Resize arbitration is now activity-based: a desktop sizing claim only
blocks phone resizes while the desktop typed within 90s
(Session.DESKTOP_CLAIM_IDLE_MS). Idle desktop -> phone takes the pane;
next desktop keystroke re-asserts the desktop layout server-side
(noteDesktopActivity via ws-routes input). Phones re-send dims every
30s (visible tab only, skipped while the keyboard is open) so attaching
under a hot claim self-corrects. Fixes the desktop-width-stream-in-
narrow-xterm soup (mid-word wraps, tmux dot fill, Ink overdraw).
- Cross-device reflows (takeover/re-assert) emit a debounced needsRefresh
so all clients reload the buffer instead of stacking ghost Ink frames.
- Keyboard accessory/toolbar lift restored: measure keyboardOffset
against window.innerHeight (layout viewport), not the shrunken .app -
on iOS the offset computed to 0, leaving both bars hidden behind the
OS keyboard with a dead gap above.
- Removed the mobile header utility ("three dots") toggle entirely;
the headerRight tray stays collapsed on small viewports.
Tests: usage-limit-patterns (36), session-auto-resume (21), resize
arbitration (+6), session routes (+4), respawn guard (+2); MockSession
auto-resume/sizing stubs; mobile tabs test updated for toggle removal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mobile-focused fixes for the web UI: keyboard-accessory layout and
overlap, native input visibility above the keyboard, CJK input handling,
terminal touch scrolling, tab-menu tap targets, mic-recording glow
containment, and mobile resize/keyboard-state handling on tab switch,
plus mobile visual-regression test coverage and snapshots.
Co-Authored-By: Saqeb Akhter <saqeb.akhter@gmail.com>