Nano Banana Pro
Agent skill for nano-banana-pro
The Script Kit prompt system manages UI windows that interact with running processes. Each prompt can be bound to a process, creating a coupled lifecycle where process termination should close the associated prompt. However, there are several edge cases where this coupling can break down.
Sign in to like and favorite skills
The Script Kit prompt system manages UI windows that interact with running processes. Each prompt can be bound to a process, creating a coupled lifecycle where process termination should close the associated prompt. However, there are several edge cases where this coupling can break down.
KitPrompt (
src/main/prompt.ts)
Prompts Manager (
src/main/prompts.ts)
Process Manager (
src/main/process.ts)
PTY System (
src/main/pty.ts)
// Idle prompt created on startup const idlePrompt = new KitPrompt(PromptType.idle); // When script needs a prompt const prompt = prompts.attachIdlePromptToProcess(reason, pid);
When a prompt is bound to a process:
bindToProcess(pid: number) { this.pid = pid; this.boundToProcess = true; this.startLongRunningMonitor(); // 30s check for unresponsive processes this.startProcessMonitoring(); // Periodic process alive checks this.listenForProcessExit(); // ProcessGone event listener }
graph TD A[Process Exits] --> B[Child Process 'exit' Event] B --> C[process.ts: child.once'exit'] C --> D[Send EXIT to Prompt] C --> E[Emit TERM_KILL Event] C --> F[removeByPid Called] F --> G[prompts.get pid.close] F --> H[Emit ProcessGone Event] H --> I[Prompt handleProcessGone] I --> J[Prompt Cleanup & Hide]
When a process is killed from terminal (e.g., Ctrl+C in PTY):
graph TD A[Terminal Kill Signal] --> B[PTY Kills Process] B --> C[Process Exit Event] C --> D[removeByPid with 1s Debounce] D --> E{Debounce Active?} E -->|Yes| F[Wait/Skip Cleanup] E -->|No| G[Close Prompt] F --> H[❌ Prompt Remains Open] G --> I[✓ Prompt Closes]
Location:
src/main/process.ts:removeByPid()
// Problematic debounce mechanism if (this.pidDebounceMap.has(pid)) { log('process.removeByPid already debounced', pid); return; } this.pidDebounceMap.set(pid, setTimeout(() => { this.clearDebounceTimeout(pid); }, 1000));
Issue: If multiple cleanup attempts occur within 1 second, subsequent attempts are ignored, potentially leaving prompts open.
Location:
src/main/prompt.ts:close()
if (!kitState.allowQuit) { if (this.boundToProcess) { if (this.hasBeenFocused) { // continues with close } else { this.resetState(); return; // ❌ Early return prevents cleanup! } } }
Issue: Unfocused prompts might not close properly when their process exits.
Location:
src/main/prompt.ts:startProcessMonitoring()
setTimeout(() => { if (this.boundToProcess && this.pid) { this.processMonitorTimer = setInterval(() => { this.checkProcessAlive(); }, this.processCheckInterval); } }, 3000); // 3 second delay before monitoring starts
Issue: Processes killed within the first 3 seconds might not trigger prompt cleanup.
The event-based cleanup relies on:
removeByPid() → prompt.close()ProcessGone event → prompt.handleProcessGone()If either path fails (due to timing, debouncing, or early returns), the prompt remains open.
removeByPid() can block cleanupProcessGone event might not reach prompt if it's already partially cleanedhideInstantCoolingDown flag might prevent hiding| Process State | Expected Prompt State | Actual State (Bug) | Cause |
|---|---|---|---|
| Running | Visible/Hidden based on interaction | ✓ Works | - |
| Exiting normally | Closing/Hiding | ✓ Works | - |
| Killed from terminal | Should close immediately | ❌ May remain open | Debounce/timing |
| Crashed | Should close after detection | ❌ May remain open | 3s monitoring delay |
| Zombie process | Should close after 30s | ✓ Works | Long-running monitor |
// In process.removeByPid() prompts.get(pid)?.close('process.removeByPid');
// ProcessGone event listener in prompt emitter.once(KitEvent.ProcessGone, () => { this.handleProcessGone(); });
// In removeByPid() const isTerminalKill = reason.includes('TERM_KILL'); if (!isTerminalKill && this.pidDebounceMap.has(pid)) { return; }
// Add direct cleanup call const prompt = prompts.get(pid); if (prompt && !prompt.isDestroyed()) { prompt.handleProcessGone(); // Direct call prompt.close('process-exit-forced'); }
// In prompt.close() if (reason.includes('process-exit') || reason.includes('TERM_KILL')) { // Skip all guards for process termination this.forceClose(); return; }
// Before showing prompt if (this.boundToProcess && this.pid) { const alive = await isProcessAlive(this.pid); if (!alive) { this.close('process-already-dead'); return; } }
Enable debug logging to trace the issue:
# Check these log files ~/Library/Logs/ScriptKit/main.log # General prompt lifecycle ~/Library/Logs/ScriptKit/process.log # Process termination events ~/Library/Logs/ScriptKit/term.log # Terminal kill signals
Key log messages to look for:
"process.removeByPid already debounced" - Indicates debounce blocking"Prompt close prevented" - Early return in close()"Process gone but prompt still bound" - Cleanup failure"hideInstantCoolingDown" - Hide preventionThe main issue appears to be a combination of:
The fix requires making the cleanup path more direct and removing obstacles (debounce, early returns, cooldowns) when a process is definitively terminated, especially from terminal kills.