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
+24 -11
View File
@@ -17,7 +17,10 @@ export interface PendingQuestion {
sessionId: string;
requestId: string;
originalInput: Record<string, unknown>;
timer: ReturnType<typeof setTimeout>;
/** Distinguishes AskUserQuestion prompts from ExitPlanMode plan-approval prompts. */
promptType: 'question' | 'plan';
// Questions never auto-expire (see addQuestion) — no timer to track.
timer: null;
}
export class PermissionManager {
@@ -49,6 +52,10 @@ export class PermissionManager {
this._trackPendingId(sessionId, requestId);
}
hasPermission(requestId: string): boolean {
return this.pendingPermissions.has(requestId);
}
resolvePermission(requestId: string): PendingPermission | undefined {
const pending = this.pendingPermissions.get(requestId);
if (!pending) return undefined;
@@ -60,25 +67,31 @@ export class PermissionManager {
// === Questions ===
addQuestion(requestId: string, sessionId: string, data: { originalInput: Record<string, unknown> }): void {
const timer = setTimeout(() => {
this.pendingQuestions.delete(requestId);
this._removePendingId(sessionId, requestId);
}, this.timeoutMs);
addQuestion(requestId: string, sessionId: string, data: { originalInput: Record<string, unknown>; promptType?: 'question' | 'plan' }): void {
// Unlike permissions (which pair with a client-side auto-deny countdown
// that reaches the same end state), nothing ever answers a question on
// a timeout — the CLI just keeps waiting indefinitely. Silently dropping
// our tracking after timeoutMs would make ClawTap forget a prompt that's
// still live on screen, producing a "ghost" the UI can never show again.
// Questions are cleaned up via resolveQuestion/dismissAll (session end),
// never by elapsed time.
this.pendingQuestions.set(requestId, {
sessionId,
requestId,
originalInput: data.originalInput,
timer,
promptType: data.promptType || 'question',
timer: null,
});
this._trackPendingId(sessionId, requestId);
}
hasQuestion(requestId: string): boolean {
return this.pendingQuestions.has(requestId);
}
resolveQuestion(requestId: string): PendingQuestion | undefined {
const pending = this.pendingQuestions.get(requestId);
if (!pending) return undefined;
clearTimeout(pending.timer);
this.pendingQuestions.delete(requestId);
this._removePendingId(pending.sessionId, requestId);
return pending;
@@ -118,7 +131,7 @@ export class PermissionManager {
const perm = this.pendingPermissions.get(reqId);
if (perm) { clearTimeout(perm.timer); this.pendingPermissions.delete(reqId); }
const q = this.pendingQuestions.get(reqId);
if (q) { clearTimeout(q.timer); this.pendingQuestions.delete(reqId); }
if (q) { this.pendingQuestions.delete(reqId); }
}
this.sessionPendingIds.delete(sessionId);
}
@@ -139,7 +152,7 @@ export class PermissionManager {
resolved.push(reqId);
}
const q = this.pendingQuestions.get(reqId);
if (q) { clearTimeout(q.timer); this.pendingQuestions.delete(reqId); }
if (q) { this.pendingQuestions.delete(reqId); }
}
this.sessionPendingIds.delete(sessionId);
return resolved;