mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
feat: add respawn controller verbosity with countdown timers and bug fixes
- Fix "enable twice" bug: add enabled:true to getModalRespawnConfig() - Add timer tracking with countdown display in respawn banner - New events: timerStarted, timerCancelled, timerCompleted, actionLog - Show active timers with progress bars and recent action log - Skip remaining timeouts after AI check returns IDLE/WORKING verdict - Enhanced DetectionStatus with activeTimers, recentActions, currentPhase Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+171
-1
@@ -361,6 +361,9 @@ class ClaudemanApp {
|
||||
this.activeSessionId = null;
|
||||
this.respawnStatus = {};
|
||||
this.respawnTimers = {}; // Track timed respawn timers
|
||||
this.respawnCountdownTimers = {}; // { sessionId: { timerName: { endsAt, totalMs, reason } } }
|
||||
this.respawnActionLogs = {}; // { sessionId: [action, action, ...] } (max 20)
|
||||
this.timerCountdownInterval = null; // Interval for updating countdown display
|
||||
this.terminalBuffers = new Map(); // Store terminal content per session
|
||||
this.editingSessionId = null; // Session being edited in options modal
|
||||
this.pendingCloseSessionId = null; // Session pending close confirmation
|
||||
@@ -977,7 +980,7 @@ class ClaudemanApp {
|
||||
}
|
||||
});
|
||||
|
||||
// Respawn timer events
|
||||
// Respawn run timer events (timed respawn runs)
|
||||
this.eventSource.addEventListener('respawn:timerStarted', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
this.respawnTimers[data.sessionId] = {
|
||||
@@ -990,6 +993,60 @@ class ClaudemanApp {
|
||||
}
|
||||
});
|
||||
|
||||
// Respawn controller countdown timer events (internal timers)
|
||||
this.eventSource.addEventListener('respawn:timerStarted', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
// This may fire for both run timers and controller timers - check for timer object
|
||||
if (data.timer) {
|
||||
const { sessionId, timer } = data;
|
||||
if (!this.respawnCountdownTimers[sessionId]) {
|
||||
this.respawnCountdownTimers[sessionId] = {};
|
||||
}
|
||||
this.respawnCountdownTimers[sessionId][timer.name] = {
|
||||
endsAt: timer.endsAt,
|
||||
totalMs: timer.durationMs,
|
||||
reason: timer.reason
|
||||
};
|
||||
if (sessionId === this.activeSessionId) {
|
||||
this.updateCountdownTimerDisplay();
|
||||
this.startCountdownInterval();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:timerCancelled', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const { sessionId, timerName, reason } = data;
|
||||
if (this.respawnCountdownTimers[sessionId]) {
|
||||
delete this.respawnCountdownTimers[sessionId][timerName];
|
||||
}
|
||||
if (sessionId === this.activeSessionId) {
|
||||
this.updateCountdownTimerDisplay();
|
||||
// Add to action log
|
||||
this.addActionLogEntry(sessionId, { type: 'timer-cancel', detail: `${timerName}${reason ? ': ' + reason : ''}`, timestamp: Date.now() });
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:timerCompleted', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const { sessionId, timerName } = data;
|
||||
if (this.respawnCountdownTimers[sessionId]) {
|
||||
delete this.respawnCountdownTimers[sessionId][timerName];
|
||||
}
|
||||
if (sessionId === this.activeSessionId) {
|
||||
this.updateCountdownTimerDisplay();
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:actionLog', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const { sessionId, action } = data;
|
||||
this.addActionLogEntry(sessionId, action);
|
||||
if (sessionId === this.activeSessionId) {
|
||||
this.updateActionLogDisplay();
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-clear event
|
||||
this.eventSource.addEventListener('session:autoClear', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
@@ -1468,8 +1525,15 @@ class ClaudemanApp {
|
||||
this.showRespawnBanner();
|
||||
this.updateRespawnBanner(this.respawnStatus[sessionId].state);
|
||||
document.getElementById('respawnCycleCount').textContent = this.respawnStatus[sessionId].cycleCount || 0;
|
||||
// Update countdown timers and action log for this session
|
||||
this.updateCountdownTimerDisplay();
|
||||
this.updateActionLogDisplay();
|
||||
if (Object.keys(this.respawnCountdownTimers[sessionId] || {}).length > 0) {
|
||||
this.startCountdownInterval();
|
||||
}
|
||||
} else {
|
||||
this.hideRespawnBanner();
|
||||
this.stopCountdownInterval();
|
||||
}
|
||||
|
||||
// Update task panel if open
|
||||
@@ -1501,6 +1565,7 @@ class ClaudemanApp {
|
||||
this.sessions.delete(sessionId);
|
||||
this.terminalBuffers.delete(sessionId);
|
||||
this.ralphStates.delete(sessionId);
|
||||
this.clearCountdownTimers(sessionId);
|
||||
|
||||
if (this.activeSessionId === sessionId) {
|
||||
this.activeSessionId = null;
|
||||
@@ -2011,11 +2076,112 @@ class ClaudemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Countdown Timer Display Methods ==========
|
||||
|
||||
addActionLogEntry(sessionId, action) {
|
||||
if (!this.respawnActionLogs[sessionId]) {
|
||||
this.respawnActionLogs[sessionId] = [];
|
||||
}
|
||||
this.respawnActionLogs[sessionId].unshift(action);
|
||||
if (this.respawnActionLogs[sessionId].length > 20) {
|
||||
this.respawnActionLogs[sessionId].pop();
|
||||
}
|
||||
}
|
||||
|
||||
startCountdownInterval() {
|
||||
if (this.timerCountdownInterval) return;
|
||||
this.timerCountdownInterval = setInterval(() => {
|
||||
if (this.activeSessionId && this.respawnCountdownTimers[this.activeSessionId]) {
|
||||
this.updateCountdownTimerDisplay();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
stopCountdownInterval() {
|
||||
if (this.timerCountdownInterval) {
|
||||
clearInterval(this.timerCountdownInterval);
|
||||
this.timerCountdownInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateCountdownTimerDisplay() {
|
||||
const timersRow = this.$('respawnTimersRow');
|
||||
const timersContainer = this.$('respawnCountdownTimers');
|
||||
if (!timersRow || !timersContainer) return;
|
||||
|
||||
const timers = this.respawnCountdownTimers[this.activeSessionId];
|
||||
if (!timers || Object.keys(timers).length === 0) {
|
||||
timersRow.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
timersRow.style.display = '';
|
||||
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));
|
||||
|
||||
// Format timer name for display (replace hyphens with spaces, capitalize first letter)
|
||||
const displayName = name.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase());
|
||||
|
||||
html += `<div class="respawn-countdown-timer" title="${timer.reason || ''}">
|
||||
<span class="timer-name">${displayName}</span>
|
||||
<span class="timer-value">${remainingSec}s</span>
|
||||
<div class="respawn-timer-bar">
|
||||
<div class="respawn-timer-progress" style="width: ${percent}%"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
timersContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
updateActionLogDisplay() {
|
||||
const logContainer = this.$('respawnActionLog');
|
||||
if (!logContainer) return;
|
||||
|
||||
const actions = this.respawnActionLogs[this.activeSessionId];
|
||||
if (!actions || actions.length === 0) {
|
||||
logContainer.innerHTML = '<span class="action-empty">No recent actions</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const action of actions.slice(0, 10)) {
|
||||
const time = new Date(action.timestamp).toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
html += `<div class="respawn-action-entry">
|
||||
<span class="action-time">${time}</span>
|
||||
<span class="action-type">[${action.type}]</span>
|
||||
<span class="action-detail">${action.detail}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
logContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
clearCountdownTimers(sessionId) {
|
||||
delete this.respawnCountdownTimers[sessionId];
|
||||
delete this.respawnActionLogs[sessionId];
|
||||
if (sessionId === this.activeSessionId) {
|
||||
this.updateCountdownTimerDisplay();
|
||||
this.updateActionLogDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
async stopRespawn() {
|
||||
if (!this.activeSessionId) return;
|
||||
try {
|
||||
await fetch(`/api/sessions/${this.activeSessionId}/respawn/stop`, { method: 'POST' });
|
||||
delete this.respawnTimers[this.activeSessionId];
|
||||
this.clearCountdownTimers(this.activeSessionId);
|
||||
} catch (err) {
|
||||
this.showToast('Failed to stop respawn', 'error');
|
||||
}
|
||||
@@ -2042,6 +2208,9 @@ class ClaudemanApp {
|
||||
this.terminalBuffers.clear();
|
||||
this.activeSessionId = null;
|
||||
this.respawnStatus = {};
|
||||
this.respawnCountdownTimers = {};
|
||||
this.respawnActionLogs = {};
|
||||
this.stopCountdownInterval();
|
||||
this.hideRespawnBanner();
|
||||
this.renderSessionTabs();
|
||||
this.terminal.clear();
|
||||
@@ -2397,6 +2566,7 @@ class ClaudemanApp {
|
||||
|
||||
return {
|
||||
respawnConfig: {
|
||||
enabled: true, // Fix: ensure enabled is set so pre-saved configs with enabled: false get overridden
|
||||
updatePrompt,
|
||||
sendClear,
|
||||
sendInit,
|
||||
|
||||
+23
-13
@@ -67,19 +67,29 @@
|
||||
|
||||
<!-- Respawn Banner -->
|
||||
<div class="respawn-banner" id="respawnBanner" style="display: none;">
|
||||
<span class="respawn-indicator">↻</span>
|
||||
<span class="respawn-state" id="respawnState">Watching</span>
|
||||
<span class="respawn-detection" id="respawnDetection">
|
||||
<span class="detection-status" id="detectionStatus"></span>
|
||||
<span class="detection-waiting" id="detectionWaiting"></span>
|
||||
<span class="detection-confidence" id="detectionConfidence"></span>
|
||||
<span class="detection-ai-check" id="detectionAiCheck" style="display: none;"></span>
|
||||
</span>
|
||||
<span class="respawn-cycles">Cycle <span id="respawnCycleCount">0</span></span>
|
||||
<span class="respawn-timer" id="respawnTimer" style="display: none;"></span>
|
||||
<span class="respawn-tokens" id="respawnTokens" style="display: none;"></span>
|
||||
<span class="respawn-step" id="respawnStep"></span>
|
||||
<button class="btn-icon-only" onclick="app.stopRespawn()" title="Stop Respawn">■</button>
|
||||
<div class="respawn-banner-row">
|
||||
<span class="respawn-indicator">↻</span>
|
||||
<span class="respawn-state" id="respawnState">Watching</span>
|
||||
<span class="respawn-detection" id="respawnDetection">
|
||||
<span class="detection-status" id="detectionStatus"></span>
|
||||
<span class="detection-waiting" id="detectionWaiting"></span>
|
||||
<span class="detection-confidence" id="detectionConfidence"></span>
|
||||
<span class="detection-ai-check" id="detectionAiCheck" style="display: none;"></span>
|
||||
</span>
|
||||
<span class="respawn-cycles">Cycle <span id="respawnCycleCount">0</span></span>
|
||||
<span class="respawn-timer" id="respawnTimer" style="display: none;"></span>
|
||||
<span class="respawn-tokens" id="respawnTokens" style="display: none;"></span>
|
||||
<span class="respawn-step" id="respawnStep"></span>
|
||||
<button class="btn-icon-only" onclick="app.stopRespawn()" title="Stop Respawn">■</button>
|
||||
</div>
|
||||
<div class="respawn-timers-row" id="respawnTimersRow" style="display: none;">
|
||||
<div class="respawn-countdown-timers" id="respawnCountdownTimers">
|
||||
<!-- Dynamically populated countdown timers -->
|
||||
</div>
|
||||
<div class="respawn-action-log" id="respawnActionLog">
|
||||
<!-- Recent actions scrolling list -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ralph / Todo Tracker Panel -->
|
||||
|
||||
@@ -454,8 +454,7 @@ body {
|
||||
/* Respawn Banner - Compact */
|
||||
.respawn-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-direction: column;
|
||||
padding: 0.35rem 1rem;
|
||||
background: linear-gradient(90deg, #1e3a1e, #132513);
|
||||
border-bottom: 1px solid rgba(34, 197, 94, 0.3);
|
||||
@@ -463,6 +462,104 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.respawn-banner-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.respawn-timers-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid rgba(34, 197, 94, 0.15);
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.respawn-countdown-timers {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.respawn-countdown-timer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.respawn-countdown-timer .timer-name {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.respawn-countdown-timer .timer-value {
|
||||
color: var(--green);
|
||||
font-weight: 500;
|
||||
min-width: 2.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.respawn-timer-bar {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.respawn-timer-progress {
|
||||
height: 100%;
|
||||
background: var(--green);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.respawn-action-log {
|
||||
flex: 1;
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.75;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.respawn-action-log::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.respawn-action-log::-webkit-scrollbar-thumb {
|
||||
background: rgba(34, 197, 94, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.respawn-action-entry {
|
||||
padding: 0.1rem 0;
|
||||
border-bottom: 1px solid rgba(34, 197, 94, 0.1);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.respawn-action-entry .action-time {
|
||||
color: var(--text-muted);
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.respawn-action-entry .action-type {
|
||||
color: var(--green);
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.respawn-action-entry .action-detail {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.respawn-indicator {
|
||||
color: var(--green);
|
||||
animation: spin 2s linear infinite;
|
||||
|
||||
@@ -1866,6 +1866,23 @@ export class WebServer extends EventEmitter {
|
||||
this.broadcast('respawn:planCheckFailed', { sessionId, error });
|
||||
});
|
||||
|
||||
// Timer tracking events for UI countdown display
|
||||
controller.on('timerStarted', (timer) => {
|
||||
this.broadcast('respawn:timerStarted', { sessionId, timer });
|
||||
});
|
||||
|
||||
controller.on('timerCancelled', (timerName, reason) => {
|
||||
this.broadcast('respawn:timerCancelled', { sessionId, timerName, reason });
|
||||
});
|
||||
|
||||
controller.on('timerCompleted', (timerName) => {
|
||||
this.broadcast('respawn:timerCompleted', { sessionId, timerName });
|
||||
});
|
||||
|
||||
controller.on('actionLog', (action) => {
|
||||
this.broadcast('respawn:actionLog', { sessionId, action });
|
||||
});
|
||||
|
||||
controller.on('log', (message: string) => {
|
||||
this.broadcast('respawn:log', { sessionId, message });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user