mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
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:
+68
-2
@@ -153,6 +153,9 @@ const _SSE_HANDLER_MAP = [
|
||||
[SSE_EVENTS.SESSION_IDLE, '_onSessionIdle'],
|
||||
[SSE_EVENTS.SESSION_WORKING, '_onSessionWorking'],
|
||||
[SSE_EVENTS.SESSION_AUTO_CLEAR, '_onSessionAutoClear'],
|
||||
[SSE_EVENTS.SESSION_LIMIT_PAUSE_SCHEDULED, '_onSessionLimitPauseScheduled'],
|
||||
[SSE_EVENTS.SESSION_LIMIT_RESUME, '_onSessionLimitResume'],
|
||||
[SSE_EVENTS.SESSION_LIMIT_RESUME_CANCELLED, '_onSessionLimitResumeCancelled'],
|
||||
[SSE_EVENTS.SESSION_CLI_INFO, '_onSessionCliInfo'],
|
||||
|
||||
// Scheduled runs
|
||||
@@ -576,7 +579,6 @@ class CodemanApp {
|
||||
// Apply keyboard bar mode from settings
|
||||
const _kbSettings = this.loadAppSettingsFromStorage();
|
||||
if (_kbSettings.extendedKeyboardBar) KeyboardAccessoryBar.setMode('extended');
|
||||
this.bindMobileHeaderUtilityToggle?.();
|
||||
this.applyHeaderVisibilitySettings();
|
||||
this.applyTabWrapSettings();
|
||||
this.applyMonitorVisibility();
|
||||
@@ -1768,6 +1770,33 @@ class CodemanApp {
|
||||
this._notifySession(data.sessionId, 'info', 'auto-clear', 'Auto-Cleared', `Context reset at ${(data.tokens || 0).toLocaleString()} tokens`);
|
||||
}
|
||||
|
||||
_onSessionLimitPauseScheduled(data) {
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
if (session) session.autoResumeAt = data.resumeAt;
|
||||
const at = new Date(data.resumeAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
if (data.sessionId === this.activeSessionId) {
|
||||
this.showToast(`Usage limit reached — auto-resume at ${at}`, 'warning');
|
||||
}
|
||||
this._notifySession(data.sessionId, 'warning', 'limit-pause', 'Usage Limit Reached', `Auto-resume scheduled for ${at}`);
|
||||
this.updateAutoResumeStatus(data.sessionId);
|
||||
}
|
||||
|
||||
_onSessionLimitResume(data) {
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
if (session) session.autoResumeAt = undefined;
|
||||
if (data.sessionId === this.activeSessionId) {
|
||||
this.showToast('Usage limit reset — work resumed automatically', 'success');
|
||||
}
|
||||
this._notifySession(data.sessionId, 'info', 'limit-resume', 'Auto-Resumed', 'Usage limit reset — continuing work');
|
||||
this.updateAutoResumeStatus(data.sessionId);
|
||||
}
|
||||
|
||||
_onSessionLimitResumeCancelled(data) {
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
if (session) session.autoResumeAt = undefined;
|
||||
this.updateAutoResumeStatus(data.sessionId);
|
||||
}
|
||||
|
||||
_onSessionCliInfo(data) {
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
if (session) {
|
||||
@@ -1843,6 +1872,7 @@ class CodemanApp {
|
||||
// selectSession's earlier resizes ran before this WS existed, so they
|
||||
// went over HTTP, which never claims (see ws-routes sizingToken).
|
||||
this.sendResize(sessionId)?.catch?.(() => {});
|
||||
this._startMobileResizeRetry(sessionId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1868,6 +1898,7 @@ class CodemanApp {
|
||||
this._ws = null;
|
||||
this._wsSessionId = null;
|
||||
this._wsReady = false;
|
||||
this._stopMobileResizeRetry();
|
||||
|
||||
// Reconnect on unexpected close (server restart, network blip, ping timeout).
|
||||
// Don't reconnect if we intentionally disconnected (_disconnectWs nulls onclose)
|
||||
@@ -1893,6 +1924,7 @@ class CodemanApp {
|
||||
_disconnectWs() {
|
||||
this._clearTimer('_wsReconnectTimer');
|
||||
this._wsReconnectAttempts = 0;
|
||||
this._stopMobileResizeRetry();
|
||||
if (this._ws) {
|
||||
this._ws.onclose = null; // Prevent re-entrant cleanup
|
||||
this._ws.close();
|
||||
@@ -1902,6 +1934,41 @@ class CodemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Small-viewport claim-idle retry. While a desktop sizing claim is "hot",
|
||||
* the server ignores this device's resize (Session.DESKTOP_CLAIM_IDLE_MS),
|
||||
* and the single resize sent on attach is deduped client-side — without a
|
||||
* retry, a phone that attached under an active desktop would render a
|
||||
* desktop-width stream forever. Re-send the current dims periodically (a
|
||||
* server-side no-op once the pane already matches) so the pane reflows to
|
||||
* this device shortly after the desktop goes idle. Visible-tab only: a
|
||||
* phone in a pocket must not steal the pane from an active desktop.
|
||||
*/
|
||||
_startMobileResizeRetry(sessionId) {
|
||||
this._stopMobileResizeRetry();
|
||||
const type =
|
||||
typeof MobileDetection !== 'undefined' && MobileDetection.getDeviceType
|
||||
? MobileDetection.getDeviceType()
|
||||
: 'desktop';
|
||||
if (type === 'desktop') return;
|
||||
this._mobileResizeRetryTimer = setInterval(() => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
if (!this._wsReady || this._wsSessionId !== sessionId) return;
|
||||
// Same guard as throttledResize: while the virtual keyboard is up, a
|
||||
// fit()+SIGWINCH at the shrunken row count makes Ink re-render garbage
|
||||
// and shifts the accessory toolbar mid-typing. Retry after it closes.
|
||||
if (typeof KeyboardHandler !== 'undefined' && KeyboardHandler.keyboardVisible) return;
|
||||
this.sendResize(sessionId)?.catch?.(() => {});
|
||||
}, MOBILE_RESIZE_RETRY_MS);
|
||||
}
|
||||
|
||||
_stopMobileResizeRetry() {
|
||||
if (this._mobileResizeRetryTimer) {
|
||||
clearInterval(this._mobileResizeRetryTimer);
|
||||
this._mobileResizeRetryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send input to server without blocking the keystroke flush cycle.
|
||||
* Uses a sequential promise chain to preserve character ordering
|
||||
@@ -2093,7 +2160,6 @@ class CodemanApp {
|
||||
KeyboardHandler.cleanup();
|
||||
MobileDetection.init();
|
||||
KeyboardHandler.init();
|
||||
this.bindMobileHeaderUtilityToggle?.();
|
||||
// Clear tab alerts
|
||||
this.tabAlerts.clear();
|
||||
// Clear shown completions (used for duplicate notification prevention)
|
||||
|
||||
Reference in New Issue
Block a user