feat(session,mobile): auto-resume on usage limit + mobile view fixes

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>
This commit is contained in:
arkon
2026-06-10 20:41:34 +02:00
co-authored by Claude Fable 5
parent 055f18fb66
commit 68310619a7
27 changed files with 1511 additions and 182 deletions
+36
View File
@@ -144,6 +144,42 @@ describe('RespawnController', () => {
});
});
describe('Usage-limit pause guard', () => {
it('blocks the cycle while the session is paused on a usage limit', async () => {
let cycleStarted = false;
let blockedReason: string | null = null;
controller.on('respawnCycleStarted', () => {
cycleStarted = true;
});
controller.on('respawnBlocked', (data: { reason: string }) => {
blockedReason = data.reason;
});
session.isLimitPaused = true;
controller.start();
session.simulateCompletionMessage();
await new Promise((resolve) => setTimeout(resolve, 250));
expect(cycleStarted).toBe(false);
expect(blockedReason).toBe('usage_limit');
expect(controller.state).toBe('watching');
});
it('cycles normally when the pause is not active', async () => {
let cycleStarted = false;
controller.on('respawnCycleStarted', () => {
cycleStarted = true;
});
session.isLimitPaused = false;
controller.start();
session.simulateCompletionMessage();
await new Promise((resolve) => setTimeout(resolve, 250));
expect(cycleStarted).toBe(true);
});
});
describe('Respawn Cycle', () => {
it('should start cycle when completion message detected and confirmed', async () => {
let cycleStarted = false;