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
+64 -8
View File
@@ -76,15 +76,21 @@ export class ClaudeHookConfig {
existing.hooks[event] = [...filtered, ...configs];
}
// Insert our statusLine script into the pipe chain (if not already there).
// Our script is a passthrough: reads stdin, POSTs to server (background), outputs stdin.
// - Has custom statusLine → pipe through our script first
// - No custom statusLine → don't touch it, preserve Claude Code built-in
const wrapperScript = this._ensureStatusLineScript(statuslineUrl);
// Wire up the statusLine so we receive context-window/model/cost data.
// - Has custom statusLine → pipe our passthrough script in front of it (POST + original coexist)
// - No custom statusLine install our own renderer (POST + render a simple status line),
// since Claude's built-in status line isn't invokable as a standalone command
const existingCmd = existing.statusLine?.command || '';
if (existingCmd && !existingCmd.includes(wrapperScript)) {
const wrapperScript = this._statusLineScriptPath();
const defaultScript = this._defaultStatusLineScriptPath();
if (existingCmd && !existingCmd.includes(wrapperScript) && !existingCmd.includes(defaultScript)) {
this._ensureStatusLineScript(statuslineUrl);
existing.statusLine = { type: 'command', command: `${wrapperScript} | ${existingCmd}` };
console.log(`[hooks] Wrapped statusLine to POST to ${statuslineUrl}`);
} else if (!existingCmd) {
this._ensureDefaultStatusLineScript(statuslineUrl);
existing.statusLine = { type: 'command', command: defaultScript };
console.log(`[hooks] Installed default statusLine to POST to ${statuslineUrl}`);
}
writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
@@ -124,9 +130,13 @@ export class ClaudeHookConfig {
if (Object.keys(existing.hooks).length === 0) delete existing.hooks;
}
// --- Restore statusLine: remove our script from the pipe chain ---
// --- Restore statusLine ---
const wrapperScript = this._statusLineScriptPath();
if (existing.statusLine?.command?.includes(wrapperScript)) {
const defaultScript = this._defaultStatusLineScriptPath();
if (existing.statusLine?.command?.includes(defaultScript)) {
// We installed this standalone — there was nothing to restore
delete existing.statusLine;
} else if (existing.statusLine?.command?.includes(wrapperScript)) {
// Remove our script + pipe from the command string
const restored = existing.statusLine.command
.replace(`${wrapperScript} | `, '')
@@ -185,6 +195,52 @@ printf '%s' "$input"
return scriptPath;
}
/** Path to our standalone statusLine renderer (used when the user has no custom statusLine) */
private _defaultStatusLineScriptPath(): string {
return join(homedir(), '.clawtap', 'hooks', 'claude-statusline-default.sh');
}
/**
* Create or update our standalone statusLine renderer.
* POSTs the raw input to ClawTap (background) and prints a simple status line
* (model · cwd · context %) to stdout, since Claude requires a command to render
* its own output when `statusLine.type` is `command`.
*/
private _ensureDefaultStatusLineScript(statuslineUrl: string): string {
const scriptPath = this._defaultStatusLineScriptPath();
const scriptDir = join(homedir(), '.clawtap', 'hooks');
mkdirSync(scriptDir, { recursive: true });
const portCheck = this._portCheckCmd();
const curlInsecure = this.useHttps ? ' -k' : '';
const script = `#!/bin/bash
input=$(cat)
# POST to ClawTap server (non-blocking, skip if server not running)
if ${portCheck}; then
printf '%s' "$input" | curl -sf${curlInsecure} --connect-timeout 2 --max-time 5 -X POST -H 'Content-Type:application/json' -d @- ${statuslineUrl} &>/dev/null &
fi
# Render a simple status line: model · cwd · context %
printf '%s' "$input" | node -e '
let data = "";
process.stdin.on("data", c => data += c);
process.stdin.on("end", () => {
try {
const j = JSON.parse(data);
const parts = [];
if (j.model?.display_name) parts.push(j.model.display_name);
const dir = j.workspace?.current_dir || j.cwd;
if (dir) parts.push(dir.replace(process.env.HOME || "", "~"));
const pct = j.context_window?.used_percentage;
if (pct != null) parts.push(\`ctx \${Math.round(pct)}%\`);
process.stdout.write(parts.join(" · "));
} catch {}
});
'
`;
writeFileSync(scriptPath, script, { mode: 0o755 });
return scriptPath;
}
private _isOurHookEntry(entry: HookEntry, portTag: string): boolean {
const hooks = entry.hooks || [];
return hooks.some(h =>