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:
+24
-11
@@ -394,7 +394,7 @@ export async function handleIncomingMessage(conn: ClientConnection, msg: ClientM
|
||||
case WS.SET_MODEL:
|
||||
return handleSetModel(conn, msg.sessionId as string, msg.model as string);
|
||||
case WS.PLAN_RESPONSE:
|
||||
return handlePlanResponse(conn, msg.sessionId as string, msg.optionIndex as number, msg.text as string | undefined);
|
||||
return handlePlanResponse(conn, msg.sessionId as string, msg.optionIndex as number, msg.text as string | undefined, msg.requestId as string | undefined);
|
||||
case WS.PROMPT_RESPONSE:
|
||||
return handlePromptResponse(conn, msg.requestId as string, {
|
||||
selectedOption: msg.selectedOption as string | undefined,
|
||||
@@ -488,12 +488,13 @@ export async function handleAbort(conn: ClientConnection, sessionId?: string): P
|
||||
if (sid && adapter) await adapter.interrupt(sid);
|
||||
}
|
||||
|
||||
export async function handlePlanResponse(conn: ClientConnection, sessionId: string, optionIndex: number, text?: string): Promise<void> {
|
||||
export async function handlePlanResponse(conn: ClientConnection, sessionId: string, optionIndex: number, text?: string, requestId?: string): Promise<void> {
|
||||
const { adapter, sid } = await getAdapterForSession(conn, sessionId);
|
||||
if (!sid || !adapter) return;
|
||||
await adapter.respondPlan(sid, optionIndex, text);
|
||||
await adapter.respondPlan(sid, optionIndex, text, requestId);
|
||||
if (requestId) broadcast(sid, { type: WS.PROMPT_DISMISSED, requestId });
|
||||
// Broadcast synthetic user message so plan card transitions to read-only on ALL clients
|
||||
// Options: 0=bypass (YOLO), 1=manually approve, 2=text feedback
|
||||
// Options: 0=bypass (YOLO), 1=manually approve, 3=text feedback
|
||||
const labels = ['Plan approved (YOLO).', 'Plan approved.'];
|
||||
const msg = optionIndex === PLAN_OPTION.TEXT_FEEDBACK ? (text || 'Rejected.') : (labels[optionIndex] || 'Plan approved.');
|
||||
broadcast(sid, { type: WS.MESSAGE_COMPLETE, messages: [{ role: 'user', content: msg }] });
|
||||
@@ -572,6 +573,17 @@ export async function handleReconnect(conn: ClientConnection, sessionId?: string
|
||||
send(conn, { type: WS.TOOL_UPDATES, tools: pending.tools });
|
||||
}
|
||||
for (const req of pending.pendingRequests) {
|
||||
if (req.type === 'plan') {
|
||||
send(conn, {
|
||||
type: WS.INTERACTIVE_PROMPT,
|
||||
requestId: req.requestId,
|
||||
promptType: 'plan',
|
||||
title: 'Plan ready',
|
||||
toolName: req.toolName,
|
||||
toolInput: req.input,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const { type: _type, ...rest } = req as Record<string, unknown>;
|
||||
send(conn, { type: WS.PERMISSION_REQUEST, ...rest });
|
||||
}
|
||||
@@ -667,13 +679,14 @@ function registerClient(conn: ClientConnection, sessionId: string): void {
|
||||
if (set) {
|
||||
set.delete(c);
|
||||
if (set.size === 0) {
|
||||
const adapterName = sessionAdapterMap.get(sid) || DEFAULT_ADAPTER;
|
||||
const adapter = getAdapter(adapterName);
|
||||
// Only release pending permissions if session is idle — if processing,
|
||||
// the client may be refreshing and will reconnect shortly to see the overlay
|
||||
if (adapter && !adapter.isProcessing(sid)) {
|
||||
adapter.releaseAllPending(sid);
|
||||
}
|
||||
// Do NOT release pending permissions/questions here. They only exist
|
||||
// because the CLI itself posted a prompt and is genuinely waiting on
|
||||
// an answer — that's real, live CLI state, not stale UI residue.
|
||||
// Wiping our tracking just makes ClawTap forget about a prompt that's
|
||||
// still sitting on screen, so a reconnecting client sees no dialog at
|
||||
// all (a "ghost" prompt the CLI is waiting on but nothing can answer).
|
||||
// Let pending entries persist until answered, resolved by the CLI, or
|
||||
// naturally expired by PermissionManager's internal timeout.
|
||||
console.log(`[session-mgr] All clients disconnected from ${sid}, session persists`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user