mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix: prevent respawn cycle after plan mode auto-accept + enhance action log UI
- Add state check in startPlanCheck() to prevent Enter if state changed - sendAutoAcceptEnter() now cancels pending AI idle checks and resets state - Log auto-accept as [command] type for visibility in action log - Highlight command entries with blue background in action log - Show action log row when there are actions (not just timers) - Increase action log height and opacity for better visibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
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
|
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"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",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -1834,6 +1834,11 @@ export class RespawnController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.verdict === 'PLAN_MODE') {
|
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.emit('planCheckCompleted', result);
|
||||||
this.logAction('plan-check', 'Verdict: PLAN_MODE - sending Enter immediately');
|
this.logAction('plan-check', 'Verdict: PLAN_MODE - sending Enter immediately');
|
||||||
this.sendAutoAcceptEnter();
|
this.sendAutoAcceptEnter();
|
||||||
@@ -1861,6 +1866,25 @@ export class RespawnController extends EventEmitter {
|
|||||||
private sendAutoAcceptEnter(): void {
|
private sendAutoAcceptEnter(): void {
|
||||||
const msSinceOutput = Date.now() - this.lastOutputTime;
|
const msSinceOutput = Date.now() - this.lastOutputTime;
|
||||||
this.log(`Auto-accepting plan mode prompt (${msSinceOutput}ms silence, pre-filter + AI confirmed)`);
|
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.session.writeViaScreen('\r');
|
||||||
this.emit('autoAcceptSent');
|
this.emit('autoAcceptSent');
|
||||||
// Reset so we don't keep spamming Enter if Claude doesn't respond
|
// Reset so we don't keep spamming Enter if Claude doesn't respond
|
||||||
|
|||||||
+25
-15
@@ -1023,6 +1023,7 @@ class ClaudemanApp {
|
|||||||
const { sessionId, action } = data;
|
const { sessionId, action } = data;
|
||||||
this.addActionLogEntry(sessionId, action);
|
this.addActionLogEntry(sessionId, action);
|
||||||
if (sessionId === this.activeSessionId) {
|
if (sessionId === this.activeSessionId) {
|
||||||
|
this.updateCountdownTimerDisplay(); // Show row if hidden
|
||||||
this.updateActionLogDisplay();
|
this.updateActionLogDisplay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2144,7 +2145,12 @@ class ClaudemanApp {
|
|||||||
if (!timersRow || !timersContainer) return;
|
if (!timersRow || !timersContainer) return;
|
||||||
|
|
||||||
const timers = this.respawnCountdownTimers[this.activeSessionId];
|
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';
|
timersRow.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2153,21 +2159,23 @@ class ClaudemanApp {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
for (const [name, timer] of Object.entries(timers)) {
|
if (hasTimers) {
|
||||||
const remainingMs = Math.max(0, timer.endsAt - now);
|
for (const [name, timer] of Object.entries(timers)) {
|
||||||
const remainingSec = (remainingMs / 1000).toFixed(1);
|
const remainingMs = Math.max(0, timer.endsAt - now);
|
||||||
const percent = Math.max(0, Math.min(100, (remainingMs / timer.totalMs) * 100));
|
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)
|
// Format timer name for display (replace hyphens with spaces, capitalize first letter)
|
||||||
const displayName = name.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase());
|
const displayName = name.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase());
|
||||||
|
|
||||||
html += `<div class="respawn-countdown-timer" title="${timer.reason || ''}">
|
html += `<div class="respawn-countdown-timer" title="${timer.reason || ''}">
|
||||||
<span class="timer-name">${displayName}</span>
|
<span class="timer-name">${displayName}</span>
|
||||||
<span class="timer-value">${remainingSec}s</span>
|
<span class="timer-value">${remainingSec}s</span>
|
||||||
<div class="respawn-timer-bar">
|
<div class="respawn-timer-bar">
|
||||||
<div class="respawn-timer-progress" style="width: ${percent}%"></div>
|
<div class="respawn-timer-progress" style="width: ${percent}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timersContainer.innerHTML = html;
|
timersContainer.innerHTML = html;
|
||||||
@@ -2191,7 +2199,9 @@ class ClaudemanApp {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit'
|
second: '2-digit'
|
||||||
});
|
});
|
||||||
html += `<div class="respawn-action-entry">
|
const isCommand = action.type === 'command';
|
||||||
|
const extraClass = isCommand ? ' action-command' : '';
|
||||||
|
html += `<div class="respawn-action-entry${extraClass}">
|
||||||
<span class="action-time">${time}</span>
|
<span class="action-time">${time}</span>
|
||||||
<span class="action-type">[${action.type}]</span>
|
<span class="action-type">[${action.type}]</span>
|
||||||
<span class="action-detail">${action.detail}</span>
|
<span class="action-detail">${action.detail}</span>
|
||||||
|
|||||||
@@ -521,10 +521,10 @@ body {
|
|||||||
|
|
||||||
.respawn-action-log {
|
.respawn-action-log {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 60px;
|
max-height: 100px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 0.65rem;
|
font-size: 0.7rem;
|
||||||
opacity: 0.75;
|
opacity: 0.9;
|
||||||
font-family: 'SF Mono', Monaco, monospace;
|
font-family: 'SF Mono', Monaco, monospace;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
@@ -560,6 +560,24 @@ body {
|
|||||||
color: var(--text-dim);
|
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 {
|
.respawn-indicator {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
animation: spin 2s linear infinite;
|
animation: spin 2s linear infinite;
|
||||||
|
|||||||
Reference in New Issue
Block a user