diff --git a/CLAUDE.md b/CLAUDE.md index 5ce6a8ab..dc5b7243 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs. -**Version**: 0.1340 +**Version**: 0.1341 **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty diff --git a/package.json b/package.json index f81f76f2..c3e269fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1340", + "version": "0.1341", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index 63a1b469..781360af 100644 --- a/src/respawn-controller.ts +++ b/src/respawn-controller.ts @@ -1834,6 +1834,11 @@ export class RespawnController extends EventEmitter { } if (result.verdict === 'PLAN_MODE') { + // Don't send Enter if state changed (e.g., AI idle check started or respawn cycle began) + if (this._state !== 'watching') { + this.logAction('plan-check', `Verdict: PLAN_MODE but state is ${this._state}, not sending Enter`); + return; + } this.emit('planCheckCompleted', result); this.logAction('plan-check', 'Verdict: PLAN_MODE - sending Enter immediately'); this.sendAutoAcceptEnter(); @@ -1861,6 +1866,25 @@ export class RespawnController extends EventEmitter { private sendAutoAcceptEnter(): void { const msSinceOutput = Date.now() - this.lastOutputTime; this.log(`Auto-accepting plan mode prompt (${msSinceOutput}ms silence, pre-filter + AI confirmed)`); + + // Cancel any pending AI idle checks - we're about to make Claude work + if (this.aiChecker.status === 'checking') { + this.log('Cancelling AI idle check before auto-accept'); + this.aiChecker.cancel(); + } + + // Cancel completion confirmation - auto-accept takes precedence + this.cancelTrackedTimer('completion-confirm', this.completionConfirmTimer, 'auto-accept'); + this.completionConfirmTimer = null; + this.completionMessageTime = null; + + // Ensure we're in watching state (not confirming_idle or ai_checking) + if (this._state !== 'watching') { + this.setState('watching'); + } + + this.logAction('command', 'Auto-accept: ↵ Enter (plan approved)'); + this.emit('stepSent', 'auto-accept', '↵'); this.session.writeViaScreen('\r'); this.emit('autoAcceptSent'); // Reset so we don't keep spamming Enter if Claude doesn't respond diff --git a/src/web/public/app.js b/src/web/public/app.js index 5c74a95d..c4e4c6ae 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1023,6 +1023,7 @@ class ClaudemanApp { const { sessionId, action } = data; this.addActionLogEntry(sessionId, action); if (sessionId === this.activeSessionId) { + this.updateCountdownTimerDisplay(); // Show row if hidden this.updateActionLogDisplay(); } }); @@ -2144,7 +2145,12 @@ class ClaudemanApp { if (!timersRow || !timersContainer) return; const timers = this.respawnCountdownTimers[this.activeSessionId]; - if (!timers || Object.keys(timers).length === 0) { + const actions = this.respawnActionLogs[this.activeSessionId]; + const hasTimers = timers && Object.keys(timers).length > 0; + const hasActions = actions && actions.length > 0; + + // Show row if we have timers OR action log entries + if (!hasTimers && !hasActions) { timersRow.style.display = 'none'; return; } @@ -2153,21 +2159,23 @@ class ClaudemanApp { const now = Date.now(); let html = ''; - for (const [name, timer] of Object.entries(timers)) { - const remainingMs = Math.max(0, timer.endsAt - now); - const remainingSec = (remainingMs / 1000).toFixed(1); - const percent = Math.max(0, Math.min(100, (remainingMs / timer.totalMs) * 100)); + if (hasTimers) { + for (const [name, timer] of Object.entries(timers)) { + const remainingMs = Math.max(0, timer.endsAt - now); + const remainingSec = (remainingMs / 1000).toFixed(1); + const percent = Math.max(0, Math.min(100, (remainingMs / timer.totalMs) * 100)); - // Format timer name for display (replace hyphens with spaces, capitalize first letter) - const displayName = name.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase()); + // Format timer name for display (replace hyphens with spaces, capitalize first letter) + const displayName = name.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase()); - html += `
- ${displayName} - ${remainingSec}s -
-
-
-
`; + html += `
+ ${displayName} + ${remainingSec}s +
+
+
+
`; + } } timersContainer.innerHTML = html; @@ -2191,7 +2199,9 @@ class ClaudemanApp { minute: '2-digit', second: '2-digit' }); - html += `
+ const isCommand = action.type === 'command'; + const extraClass = isCommand ? ' action-command' : ''; + html += `
${time} [${action.type}] ${action.detail} diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 7b25d64d..21f47675 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -521,10 +521,10 @@ body { .respawn-action-log { flex: 1; - max-height: 60px; + max-height: 100px; overflow-y: auto; - font-size: 0.65rem; - opacity: 0.75; + font-size: 0.7rem; + opacity: 0.9; font-family: 'SF Mono', Monaco, monospace; color: var(--text-dim); } @@ -560,6 +560,24 @@ body { color: var(--text-dim); } +/* Command entries are highlighted */ +.respawn-action-entry.action-command { + background: rgba(59, 130, 246, 0.15); + border-left: 2px solid var(--accent); + padding-left: 0.3rem; + margin: 0.15rem 0; +} + +.respawn-action-entry.action-command .action-type { + color: var(--accent); + font-weight: 600; +} + +.respawn-action-entry.action-command .action-detail { + color: var(--text); + font-weight: 500; +} + .respawn-indicator { color: var(--green); animation: spin 2s linear infinite;