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:
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user