feat: plan-approval overlay, auth-expiry handling, cert download, login reset

- Add a dedicated plan-approval bottom sheet (interactive-prompt) driven by
  ExitPlanMode's now-4-option layout (Claude Code v2.1.177), with
  approve/YOLO/reject/feedback wired through respondPlan(requestId).
- PermissionManager: stop auto-expiring AskUserQuestion/plan prompts and stop
  releasing pending prompts on client disconnect — they reflect real CLI
  state and must survive reconnects.
- Detect server-rejected tokens (401/403 or failed WS handshake) and bounce
  back to the login screen via a new AUTH_EXPIRED_EVENT.
- Add `/cert` endpoint + Settings entry so phones can download/trust the
  self-signed HTTPS cert directly; fix cert IP detection on non-macOS hosts.
- Add `clawtap reset-login` command and `/api/auth/reset-attempts` endpoint
  to clear login rate-limit lockouts.
- Codex adapter: only forward recognized keystrokes/option indices to tmux,
  never the synthetic "deny" dismissal signal.
- PWA: actively poll for service-worker updates (iOS standalone apps don't
  reliably check on navigation).
- update-service.sh: install tmux if missing, prompt to create the env file
  instead of failing, add --skip-firewall, and improve logging.
This commit is contained in:
2026-06-15 16:30:24 -04:00
parent 4e6dfb4726
commit 2ded310472
23 changed files with 656 additions and 97 deletions
+11 -1
View File
@@ -20,21 +20,26 @@ export class WsClient {
private activeAdapter: string | null = null;
private visibilityHandler: (() => void) | null = null;
private hiddenSince: number | null = null;
private openedThisAttempt = false;
private onAuthCheckNeeded: (() => void) | null = null;
constructor(token: string, onMessage: MessageHandler, onStatus: StatusHandler) {
constructor(token: string, onMessage: MessageHandler, onStatus: StatusHandler, onAuthCheckNeeded?: () => void) {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
this.url = `${proto}://${location.host}/ws?token=${encodeURIComponent(token)}`;
this.onMessage = onMessage;
this.onStatus = onStatus;
this.onAuthCheckNeeded = onAuthCheckNeeded || null;
}
connect() {
this.shouldReconnect = true;
this.openedThisAttempt = false;
this.onStatus('connecting');
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.openedThisAttempt = true;
this.reconnectDelay = 1000;
this.onStatus('connected');
@@ -68,6 +73,11 @@ export class WsClient {
this.ws.onclose = () => {
this.onStatus('disconnected');
// The handshake itself failed (e.g. server rejected the auth token) —
// check whether our token is still valid rather than reconnecting forever.
if (!this.openedThisAttempt) {
this.onAuthCheckNeeded?.();
}
if (this.shouldReconnect) {
this.onStatus('reconnecting');
setTimeout(() => this.connect(), this.reconnectDelay);