mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 23:19:43 +02:00
Split the monolithic app.js (~12.5K lines) into 6 focused mixin modules that extend CodemanApp.prototype via Object.assign: - terminal-ui.js — terminal setup, rendering pipeline, controls - respawn-ui.js — respawn banner, countdown, presets, run summary - ralph-panel.js — Ralph state panel, fix_plan, plan versioning - settings-ui.js — app settings, visibility, web push, tunnel/QR, help - panels-ui.js — subagent panel, teams, insights, file browser, log viewer - session-ui.js — quick start, session options, case settings Fix deferred script init ordering: wrap CodemanApp instantiation in DOMContentLoaded so all defer'd mixin modules execute their Object.assign before the constructor runs. Without this, init() calls methods like applyHeaderVisibilitySettings() that don't exist yet. Guard missing cleanupWizardDragging() call in subagent-windows.js. Update build.mjs to minify/hash all new modules. Update CLAUDE.md with new frontend architecture and load order. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1222 lines
41 KiB
JavaScript
1222 lines
41 KiB
JavaScript
/**
|
|
* @fileoverview Ralph state panel (progress ring, status, task cards, fix_plan.md integration),
|
|
* plan versioning (checkpoint, rollback, diff), and plan wizard agents in monitor.
|
|
* Includes 11 SSE handlers for Ralph and plan events.
|
|
*
|
|
* @mixin Extends CodemanApp.prototype via Object.assign
|
|
* @dependency app.js (CodemanApp class, this.ralphStates, this.activeSessionId)
|
|
* @dependency constants.js (escapeHtml)
|
|
* @loadorder 9 of 15 — loaded after respawn-ui.js, before settings-ui.js
|
|
*/
|
|
|
|
Object.assign(CodemanApp.prototype, {
|
|
// Ralph
|
|
_onRalphLoopUpdate(data) {
|
|
// Skip if user explicitly closed this session's Ralph panel
|
|
if (this.ralphClosedSessions.has(data.sessionId)) return;
|
|
this.updateRalphState(data.sessionId, { loop: data.state });
|
|
},
|
|
|
|
_onRalphTodoUpdate(data) {
|
|
// Skip if user explicitly closed this session's Ralph panel
|
|
if (this.ralphClosedSessions.has(data.sessionId)) return;
|
|
this.updateRalphState(data.sessionId, { todos: data.todos });
|
|
},
|
|
|
|
_onRalphCompletionDetected(data) {
|
|
// Skip if user explicitly closed this session's Ralph panel
|
|
if (this.ralphClosedSessions.has(data.sessionId)) return;
|
|
// Prevent duplicate notifications for the same completion
|
|
const completionKey = `${data.sessionId}:${data.phrase}`;
|
|
if (this._shownCompletions?.has(completionKey)) {
|
|
return;
|
|
}
|
|
if (!this._shownCompletions) {
|
|
this._shownCompletions = new Set();
|
|
}
|
|
this._shownCompletions.add(completionKey);
|
|
// Clear after 30 seconds to allow re-notification if loop restarts
|
|
setTimeout(() => this._shownCompletions?.delete(completionKey), 30000);
|
|
|
|
// Update ralph state to mark loop as inactive
|
|
const existing = this.ralphStates.get(data.sessionId) || {};
|
|
if (existing.loop) {
|
|
existing.loop.active = false;
|
|
this.updateRalphState(data.sessionId, existing);
|
|
}
|
|
|
|
const session = this.sessions.get(data.sessionId);
|
|
this.notificationManager?.notify({
|
|
urgency: 'warning',
|
|
category: 'ralph-complete',
|
|
sessionId: data.sessionId,
|
|
sessionName: session?.name || this.getShortId(data.sessionId),
|
|
title: 'Loop Complete',
|
|
message: `Completion: ${data.phrase || 'unknown'}`,
|
|
});
|
|
},
|
|
|
|
_onRalphStatusUpdate(data) {
|
|
// Skip if user explicitly closed this session's Ralph panel
|
|
if (this.ralphClosedSessions.has(data.sessionId)) return;
|
|
this.updateRalphState(data.sessionId, { statusBlock: data.block });
|
|
},
|
|
|
|
_onCircuitBreakerUpdate(data) {
|
|
// Skip if user explicitly closed this session's Ralph panel
|
|
if (this.ralphClosedSessions.has(data.sessionId)) return;
|
|
this.updateRalphState(data.sessionId, { circuitBreaker: data.status });
|
|
// Notify if circuit breaker opens
|
|
if (data.status.state === 'OPEN') {
|
|
const session = this.sessions.get(data.sessionId);
|
|
this.notificationManager?.notify({
|
|
urgency: 'critical',
|
|
category: 'circuit-breaker',
|
|
sessionId: data.sessionId,
|
|
sessionName: session?.name || this.getShortId(data.sessionId),
|
|
title: 'Circuit Breaker Open',
|
|
message: data.status.reason || 'Loop stuck - no progress detected',
|
|
});
|
|
}
|
|
},
|
|
|
|
_onExitGateMet(data) {
|
|
const session = this.sessions.get(data.sessionId);
|
|
this.notificationManager?.notify({
|
|
urgency: 'warning',
|
|
category: 'exit-gate',
|
|
sessionId: data.sessionId,
|
|
sessionName: session?.name || this.getShortId(data.sessionId),
|
|
title: 'Exit Gate Met',
|
|
message: `Loop ready to exit (indicators: ${data.completionIndicators})`,
|
|
});
|
|
},
|
|
|
|
// Bash tools
|
|
|
|
// Plan orchestration
|
|
_onPlanSubagent(data) {
|
|
console.log('[Plan Subagent]', data);
|
|
this.handlePlanSubagentEvent(data);
|
|
},
|
|
|
|
_onPlanProgress(data) {
|
|
console.log('[Plan Progress]', data);
|
|
|
|
// Update UI if we have a progress handler registered
|
|
if (this._planProgressHandler) {
|
|
this._planProgressHandler({ type: 'plan:progress', data });
|
|
}
|
|
|
|
// Also update the loading display directly for better feedback
|
|
const titleEl = document.getElementById('planLoadingTitle');
|
|
const hintEl = document.getElementById('planLoadingHint');
|
|
|
|
if (titleEl && data.phase) {
|
|
const phaseLabels = {
|
|
'parallel-analysis': 'Running parallel analysis...',
|
|
'subagent': data.detail || 'Subagent working...',
|
|
'synthesis': 'Synthesizing results...',
|
|
'verification': 'Running verification...',
|
|
};
|
|
titleEl.textContent = phaseLabels[data.phase] || data.phase;
|
|
}
|
|
if (hintEl && data.detail) {
|
|
hintEl.textContent = data.detail;
|
|
}
|
|
},
|
|
|
|
_onPlanStarted(data) {
|
|
console.log('[Plan Started]', data);
|
|
this.activePlanOrchestratorId = data.orchestratorId;
|
|
this.planGenerationStopped = false; // Reset flag for new generation
|
|
this.renderMonitorPlanAgents();
|
|
},
|
|
|
|
_onPlanCancelled(data) {
|
|
console.log('[Plan Cancelled]', data);
|
|
if (this.activePlanOrchestratorId === data.orchestratorId) {
|
|
this.activePlanOrchestratorId = null;
|
|
}
|
|
this.renderMonitorPlanAgents();
|
|
},
|
|
|
|
_onPlanCompleted(data) {
|
|
console.log('[Plan Completed]', data);
|
|
if (this.activePlanOrchestratorId === data.orchestratorId) {
|
|
this.activePlanOrchestratorId = null;
|
|
}
|
|
this.renderMonitorPlanAgents();
|
|
},
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Enhanced Ralph Wiggum Loop Panel
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
updateRalphState(sessionId, updates) {
|
|
const existing = this.ralphStates.get(sessionId) || { loop: null, todos: [] };
|
|
const updated = { ...existing, ...updates };
|
|
this.ralphStates.set(sessionId, updated);
|
|
|
|
// Re-render if this is the active session
|
|
if (sessionId === this.activeSessionId) {
|
|
this.renderRalphStatePanel();
|
|
}
|
|
},
|
|
|
|
toggleRalphStatePanel() {
|
|
// Preserve xterm scroll position to prevent jump when panel height changes
|
|
const xtermViewport = this.terminal?.element?.querySelector('.xterm-viewport');
|
|
const scrollTop = xtermViewport?.scrollTop;
|
|
|
|
this.ralphStatePanelCollapsed = !this.ralphStatePanelCollapsed;
|
|
this.renderRalphStatePanel();
|
|
|
|
// Restore scroll position and refit terminal after layout change
|
|
requestAnimationFrame(() => {
|
|
// Restore xterm scroll position
|
|
if (xtermViewport && scrollTop !== undefined) {
|
|
xtermViewport.scrollTop = scrollTop;
|
|
}
|
|
// Refit terminal to new container size
|
|
if (this.terminal && this.fitAddon) {
|
|
this.fitAddon.fit();
|
|
}
|
|
});
|
|
},
|
|
|
|
async closeRalphTracker() {
|
|
if (!this.activeSessionId) return;
|
|
|
|
// Mark this session as explicitly closed - will stay hidden until user re-enables
|
|
this.ralphClosedSessions.add(this.activeSessionId);
|
|
|
|
// Disable tracker via API
|
|
await this._apiPost(`/api/sessions/${this.activeSessionId}/ralph-config`, { enabled: false });
|
|
|
|
// Clear local state and hide panel
|
|
this.ralphStates.delete(this.activeSessionId);
|
|
this.renderRalphStatePanel();
|
|
},
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// @fix_plan.md Integration
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
toggleRalphMenu() {
|
|
const dropdown = document.getElementById('ralphDropdown');
|
|
if (dropdown) {
|
|
dropdown.classList.toggle('show');
|
|
}
|
|
},
|
|
|
|
closeRalphMenu() {
|
|
const dropdown = document.getElementById('ralphDropdown');
|
|
if (dropdown) {
|
|
dropdown.classList.remove('show');
|
|
}
|
|
},
|
|
|
|
async resetCircuitBreaker() {
|
|
if (!this.activeSessionId) return;
|
|
|
|
try {
|
|
const response = await this._apiPost(`/api/sessions/${this.activeSessionId}/ralph-circuit-breaker/reset`, {});
|
|
const data = await response?.json();
|
|
|
|
if (data?.success) {
|
|
this.notificationManager?.notify({
|
|
urgency: 'info',
|
|
category: 'circuit-breaker',
|
|
title: 'Reset',
|
|
message: 'Circuit breaker reset to CLOSED',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error resetting circuit breaker:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Generate @fix_plan.md content and show in a modal.
|
|
*/
|
|
async showFixPlan() {
|
|
if (!this.activeSessionId) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/sessions/${this.activeSessionId}/fix-plan`);
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
this.notificationManager?.notify({
|
|
urgency: 'error',
|
|
category: 'fix-plan',
|
|
title: 'Error',
|
|
message: data.error || 'Failed to generate fix plan',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Show in a modal
|
|
this.showFixPlanModal(data.data.content, data.data.todoCount);
|
|
} catch (error) {
|
|
console.error('Error fetching fix plan:', error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Show fix plan content in a modal.
|
|
*/
|
|
showFixPlanModal(content, todoCount) {
|
|
// Create modal if it doesn't exist
|
|
let modal = document.getElementById('fixPlanModal');
|
|
if (!modal) {
|
|
modal = document.createElement('div');
|
|
modal.id = 'fixPlanModal';
|
|
modal.className = 'modal';
|
|
modal.innerHTML = `
|
|
<div class="modal-content fix-plan-modal">
|
|
<div class="modal-header">
|
|
<h3>@fix_plan.md</h3>
|
|
<button class="btn-close" onclick="app.closeFixPlanModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<textarea id="fixPlanContent" class="fix-plan-textarea" readonly></textarea>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<span class="fix-plan-stats" id="fixPlanStats"></span>
|
|
<button class="btn btn-secondary" onclick="app.copyFixPlan()">Copy</button>
|
|
<button class="btn btn-primary" onclick="app.writeFixPlanToFile()">Write to File</button>
|
|
<button class="btn btn-secondary" onclick="app.closeFixPlanModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
document.getElementById('fixPlanContent').value = content;
|
|
document.getElementById('fixPlanStats').textContent = `${todoCount} tasks`;
|
|
modal.classList.add('show');
|
|
},
|
|
|
|
closeFixPlanModal() {
|
|
const modal = document.getElementById('fixPlanModal');
|
|
if (modal) {
|
|
modal.classList.remove('show');
|
|
}
|
|
},
|
|
|
|
async copyFixPlan() {
|
|
const content = document.getElementById('fixPlanContent')?.value;
|
|
if (content) {
|
|
await navigator.clipboard.writeText(content);
|
|
this.notificationManager?.notify({
|
|
urgency: 'info',
|
|
category: 'fix-plan',
|
|
title: 'Copied',
|
|
message: 'Fix plan copied to clipboard',
|
|
});
|
|
}
|
|
},
|
|
|
|
async writeFixPlanToFile() {
|
|
if (!this.activeSessionId) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/sessions/${this.activeSessionId}/fix-plan/write`, {
|
|
method: 'POST',
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.notificationManager?.notify({
|
|
urgency: 'info',
|
|
category: 'fix-plan',
|
|
title: 'Written',
|
|
message: `@fix_plan.md written to ${data.data.filePath}`,
|
|
});
|
|
this.closeFixPlanModal();
|
|
} else {
|
|
this.notificationManager?.notify({
|
|
urgency: 'error',
|
|
category: 'fix-plan',
|
|
title: 'Error',
|
|
message: data.error || 'Failed to write file',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error writing fix plan:', error);
|
|
}
|
|
},
|
|
|
|
async importFixPlanFromFile() {
|
|
if (!this.activeSessionId) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/sessions/${this.activeSessionId}/fix-plan/read`, {
|
|
method: 'POST',
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.notificationManager?.notify({
|
|
urgency: 'info',
|
|
category: 'fix-plan',
|
|
title: 'Imported',
|
|
message: `Imported ${data.data.importedCount} tasks from @fix_plan.md`,
|
|
});
|
|
// Refresh ralph panel
|
|
this.updateRalphState(this.activeSessionId, { todos: data.data.todos });
|
|
} else {
|
|
this.notificationManager?.notify({
|
|
urgency: 'warning',
|
|
category: 'fix-plan',
|
|
title: 'Not Found',
|
|
message: data.error || '@fix_plan.md not found',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error importing fix plan:', error);
|
|
}
|
|
},
|
|
|
|
toggleRalphDetach() {
|
|
const panel = this.$('ralphStatePanel');
|
|
const detachBtn = this.$('ralphDetachBtn');
|
|
|
|
if (!panel) return;
|
|
|
|
if (panel.classList.contains('detached')) {
|
|
// Re-attach to original position
|
|
panel.classList.remove('detached');
|
|
panel.style.top = '';
|
|
panel.style.left = '';
|
|
panel.style.width = '';
|
|
panel.style.height = '';
|
|
if (detachBtn) {
|
|
detachBtn.innerHTML = '⧉'; // Detach icon (two overlapping squares)
|
|
detachBtn.title = 'Detach panel';
|
|
}
|
|
} else {
|
|
// Detach as floating window
|
|
panel.classList.add('detached');
|
|
// Expand when detaching for better visibility
|
|
this.ralphStatePanelCollapsed = false;
|
|
panel.classList.remove('collapsed');
|
|
if (detachBtn) {
|
|
detachBtn.innerHTML = '⊞'; // Attach icon (squared plus - dock back)
|
|
detachBtn.title = 'Attach panel';
|
|
}
|
|
// Setup drag functionality
|
|
this.setupRalphDrag();
|
|
}
|
|
this.renderRalphStatePanel();
|
|
},
|
|
|
|
setupRalphDrag() {
|
|
const panel = this.$('ralphStatePanel');
|
|
const header = this.$('ralphSummary');
|
|
|
|
if (!panel || !header) return;
|
|
|
|
let isDragging = false;
|
|
let startX, startY, startLeft, startTop;
|
|
|
|
const onMouseDown = (e) => {
|
|
// Only drag from header, not from buttons or toggle
|
|
if (e.target.closest('button') || e.target.closest('.ralph-toggle')) return;
|
|
if (!panel.classList.contains('detached')) return;
|
|
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
const rect = panel.getBoundingClientRect();
|
|
startLeft = rect.left;
|
|
startTop = rect.top;
|
|
|
|
document.addEventListener('mousemove', onMouseMove);
|
|
document.addEventListener('mouseup', onMouseUp);
|
|
e.preventDefault();
|
|
};
|
|
|
|
const onMouseMove = (e) => {
|
|
if (!isDragging) return;
|
|
|
|
const dx = e.clientX - startX;
|
|
const dy = e.clientY - startY;
|
|
|
|
let newLeft = startLeft + dx;
|
|
let newTop = startTop + dy;
|
|
|
|
// Keep within viewport bounds
|
|
const rect = panel.getBoundingClientRect();
|
|
newLeft = Math.max(0, Math.min(window.innerWidth - rect.width, newLeft));
|
|
newTop = Math.max(0, Math.min(window.innerHeight - rect.height, newTop));
|
|
|
|
panel.style.left = newLeft + 'px';
|
|
panel.style.top = newTop + 'px';
|
|
};
|
|
|
|
const onMouseUp = () => {
|
|
isDragging = false;
|
|
document.removeEventListener('mousemove', onMouseMove);
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
};
|
|
|
|
// Remove existing listeners before adding new ones
|
|
header.removeEventListener('mousedown', header._ralphDragHandler);
|
|
header._ralphDragHandler = onMouseDown;
|
|
header.addEventListener('mousedown', onMouseDown);
|
|
},
|
|
|
|
renderRalphStatePanel() {
|
|
// Debounce renders at 50ms to prevent excessive DOM updates
|
|
if (this.renderRalphStatePanelTimeout) {
|
|
clearTimeout(this.renderRalphStatePanelTimeout);
|
|
}
|
|
this.renderRalphStatePanelTimeout = setTimeout(() => {
|
|
this._renderRalphStatePanelImmediate();
|
|
}, 50);
|
|
},
|
|
|
|
_renderRalphStatePanelImmediate() {
|
|
const panel = this.$('ralphStatePanel');
|
|
const toggle = this.$('ralphToggle');
|
|
|
|
if (!panel) return;
|
|
|
|
// If user explicitly closed this session's Ralph panel, keep it hidden
|
|
if (this.ralphClosedSessions.has(this.activeSessionId)) {
|
|
panel.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const state = this.ralphStates.get(this.activeSessionId);
|
|
|
|
// Check if there's anything to show
|
|
// Only show panel if tracker is enabled OR there's active state to display
|
|
const isEnabled = state?.loop?.enabled === true;
|
|
const hasLoop = state?.loop?.active || state?.loop?.completionPhrase;
|
|
const hasTodos = state?.todos?.length > 0;
|
|
const hasCircuitBreaker = state?.circuitBreaker && state.circuitBreaker.state !== 'CLOSED';
|
|
const hasStatusBlock = state?.statusBlock !== undefined;
|
|
|
|
if (!isEnabled && !hasLoop && !hasTodos && !hasCircuitBreaker && !hasStatusBlock) {
|
|
panel.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
panel.style.display = '';
|
|
|
|
// Calculate completion percentage
|
|
const todos = state?.todos || [];
|
|
const completed = todos.filter(t => t.status === 'completed').length;
|
|
const total = todos.length;
|
|
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
|
|
// Update progress rings
|
|
this.updateRalphRing(percent);
|
|
|
|
// Update status badge (pass completion info)
|
|
this.updateRalphStatus(state?.loop, completed, total);
|
|
|
|
// Update stats
|
|
this.updateRalphStats(state?.loop, completed, total);
|
|
|
|
// Update circuit breaker badge
|
|
this.updateCircuitBreakerBadge(state?.circuitBreaker);
|
|
|
|
// Handle collapsed/expanded state
|
|
if (this.ralphStatePanelCollapsed) {
|
|
panel.classList.add('collapsed');
|
|
if (toggle) toggle.innerHTML = '▼'; // Down arrow when collapsed (click to expand)
|
|
} else {
|
|
panel.classList.remove('collapsed');
|
|
if (toggle) toggle.innerHTML = '▲'; // Up arrow when expanded (click to collapse)
|
|
|
|
// Update expanded view content
|
|
this.updateRalphExpandedView(state);
|
|
}
|
|
},
|
|
|
|
updateRalphRing(percent) {
|
|
// Ensure percent is a valid number between 0-100
|
|
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
|
|
|
|
// Mini ring (in summary)
|
|
const miniProgress = this.$('ralphRingMiniProgress');
|
|
const miniText = this.$('ralphRingMiniText');
|
|
if (miniProgress) {
|
|
// Circumference = 2 * PI * r = 2 * PI * 15.9 ≈ 100
|
|
// offset = 100 means 0% visible, offset = 0 means 100% visible
|
|
const offset = 100 - safePercent;
|
|
miniProgress.style.strokeDashoffset = offset;
|
|
}
|
|
if (miniText) {
|
|
miniText.textContent = `${safePercent}%`;
|
|
}
|
|
|
|
// Large ring (in expanded view)
|
|
const largeProgress = this.$('ralphRingProgress');
|
|
const largePercent = this.$('ralphRingPercent');
|
|
if (largeProgress) {
|
|
// Circumference = 2 * PI * r = 2 * PI * 42 ≈ 264
|
|
// offset = 264 means 0% visible, offset = 0 means 100% visible
|
|
const offset = 264 - (264 * safePercent / 100);
|
|
largeProgress.style.strokeDashoffset = offset;
|
|
}
|
|
if (largePercent) {
|
|
largePercent.textContent = `${safePercent}%`;
|
|
}
|
|
},
|
|
|
|
updateRalphStatus(loop, completed = 0, total = 0) {
|
|
const badge = this.$('ralphStatusBadge');
|
|
const statusText = badge?.querySelector('.ralph-status-text');
|
|
if (!badge || !statusText) return;
|
|
|
|
badge.classList.remove('active', 'completed', 'tracking');
|
|
|
|
if (loop?.active) {
|
|
badge.classList.add('active');
|
|
statusText.textContent = 'Running';
|
|
} else if (total > 0 && completed === total) {
|
|
// Only show "Complete" when all todos are actually done
|
|
badge.classList.add('completed');
|
|
statusText.textContent = 'Complete';
|
|
} else if (loop?.enabled || total > 0) {
|
|
badge.classList.add('tracking');
|
|
statusText.textContent = 'Tracking';
|
|
} else {
|
|
statusText.textContent = 'Idle';
|
|
}
|
|
},
|
|
|
|
updateCircuitBreakerBadge(circuitBreaker) {
|
|
// Find or create the circuit breaker badge container
|
|
let cbContainer = this.$('ralphCircuitBreakerBadge');
|
|
if (!cbContainer) {
|
|
// Create container if it doesn't exist (we'll add it dynamically)
|
|
const summary = this.$('ralphSummary');
|
|
if (!summary) return;
|
|
|
|
// Check if it already exists
|
|
cbContainer = summary.querySelector('.ralph-circuit-breaker');
|
|
if (!cbContainer) {
|
|
cbContainer = document.createElement('div');
|
|
cbContainer.id = 'ralphCircuitBreakerBadge';
|
|
cbContainer.className = 'ralph-circuit-breaker';
|
|
// Insert after the status badge
|
|
const statusBadge = this.$('ralphStatusBadge');
|
|
if (statusBadge && statusBadge.nextSibling) {
|
|
statusBadge.parentNode.insertBefore(cbContainer, statusBadge.nextSibling);
|
|
} else {
|
|
summary.appendChild(cbContainer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hide if no circuit breaker state or CLOSED
|
|
if (!circuitBreaker || circuitBreaker.state === 'CLOSED') {
|
|
cbContainer.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
cbContainer.style.display = '';
|
|
cbContainer.classList.remove('half-open', 'open');
|
|
|
|
if (circuitBreaker.state === 'HALF_OPEN') {
|
|
cbContainer.classList.add('half-open');
|
|
cbContainer.innerHTML = `<span class="cb-icon">⚠</span><span class="cb-text">Warning</span>`;
|
|
cbContainer.title = circuitBreaker.reason || 'Circuit breaker warning';
|
|
} else if (circuitBreaker.state === 'OPEN') {
|
|
cbContainer.classList.add('open');
|
|
cbContainer.innerHTML = `<span class="cb-icon">🛑</span><span class="cb-text">Stuck</span>`;
|
|
cbContainer.title = circuitBreaker.reason || 'Loop appears stuck';
|
|
}
|
|
|
|
// Add click handler to reset
|
|
cbContainer.onclick = () => this.resetCircuitBreaker();
|
|
},
|
|
|
|
|
|
updateRalphStats(loop, completed, total) {
|
|
// Time stat
|
|
const timeEl = this.$('ralphStatTime');
|
|
if (timeEl) {
|
|
if (loop?.elapsedHours !== null && loop?.elapsedHours !== undefined) {
|
|
timeEl.textContent = this.formatRalphTime(loop.elapsedHours);
|
|
} else if (loop?.startedAt) {
|
|
const hours = (Date.now() - loop.startedAt) / (1000 * 60 * 60);
|
|
timeEl.textContent = this.formatRalphTime(hours);
|
|
} else {
|
|
timeEl.textContent = '0m';
|
|
}
|
|
}
|
|
|
|
// Cycles stat
|
|
const cyclesEl = this.$('ralphStatCycles');
|
|
if (cyclesEl) {
|
|
if (loop?.maxIterations) {
|
|
cyclesEl.textContent = `${loop.cycleCount || 0}/${loop.maxIterations}`;
|
|
} else {
|
|
cyclesEl.textContent = String(loop?.cycleCount || 0);
|
|
}
|
|
}
|
|
|
|
// Tasks stat
|
|
const tasksEl = this.$('ralphStatTasks');
|
|
if (tasksEl) {
|
|
tasksEl.textContent = `${completed}/${total}`;
|
|
}
|
|
},
|
|
|
|
formatRalphTime(hours) {
|
|
if (hours < 0.0167) return '0m'; // < 1 minute
|
|
if (hours < 1) {
|
|
const minutes = Math.round(hours * 60);
|
|
return `${minutes}m`;
|
|
}
|
|
const h = Math.floor(hours);
|
|
const m = Math.round((hours - h) * 60);
|
|
if (m === 0) return `${h}h`;
|
|
return `${h}h ${m}m`;
|
|
},
|
|
|
|
updateRalphExpandedView(state) {
|
|
// Update phrase
|
|
const phraseEl = this.$('ralphPhrase');
|
|
if (phraseEl) {
|
|
phraseEl.textContent = state?.loop?.completionPhrase || '--';
|
|
}
|
|
|
|
// Update elapsed
|
|
const elapsedEl = this.$('ralphElapsed');
|
|
if (elapsedEl) {
|
|
if (state?.loop?.elapsedHours !== null && state?.loop?.elapsedHours !== undefined) {
|
|
elapsedEl.textContent = this.formatRalphTime(state.loop.elapsedHours);
|
|
} else if (state?.loop?.startedAt) {
|
|
const hours = (Date.now() - state.loop.startedAt) / (1000 * 60 * 60);
|
|
elapsedEl.textContent = this.formatRalphTime(hours);
|
|
} else {
|
|
elapsedEl.textContent = '0m';
|
|
}
|
|
}
|
|
|
|
// Update iterations
|
|
const iterationsEl = this.$('ralphIterations');
|
|
if (iterationsEl) {
|
|
if (state?.loop?.maxIterations) {
|
|
iterationsEl.textContent = `${state.loop.cycleCount || 0} / ${state.loop.maxIterations}`;
|
|
} else {
|
|
iterationsEl.textContent = String(state?.loop?.cycleCount || 0);
|
|
}
|
|
}
|
|
|
|
// Update tasks count
|
|
const todos = state?.todos || [];
|
|
const completed = todos.filter(t => t.status === 'completed').length;
|
|
const tasksCountEl = this.$('ralphTasksCount');
|
|
if (tasksCountEl) {
|
|
tasksCountEl.textContent = `${completed}/${todos.length}`;
|
|
}
|
|
|
|
// Update plan version display if available
|
|
if (state?.loop?.planVersion) {
|
|
this.updatePlanVersionDisplay(state.loop.planVersion, state.loop.planHistoryLength || 1);
|
|
} else {
|
|
this.updatePlanVersionDisplay(null, 0);
|
|
}
|
|
|
|
// Render task cards
|
|
this.renderRalphTasks(todos);
|
|
|
|
// Render RALPH_STATUS block if present
|
|
this.renderRalphStatusBlock(state?.statusBlock);
|
|
},
|
|
|
|
renderRalphStatusBlock(statusBlock) {
|
|
// Find or create the status block container
|
|
let container = this.$('ralphStatusBlockDisplay');
|
|
const expandedContent = this.$('ralphExpandedContent');
|
|
|
|
if (!statusBlock) {
|
|
// Remove container if no status block
|
|
if (container) {
|
|
container.remove();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!container && expandedContent) {
|
|
container = document.createElement('div');
|
|
container.id = 'ralphStatusBlockDisplay';
|
|
container.className = 'ralph-status-block';
|
|
// Insert at the top of expanded content
|
|
expandedContent.insertBefore(container, expandedContent.firstChild);
|
|
}
|
|
|
|
if (!container) return;
|
|
|
|
// Build status class
|
|
const statusClass = statusBlock.status === 'IN_PROGRESS' ? 'in-progress'
|
|
: statusBlock.status === 'COMPLETE' ? 'complete'
|
|
: statusBlock.status === 'BLOCKED' ? 'blocked' : '';
|
|
|
|
// Build tests status icon
|
|
const testsIcon = statusBlock.testsStatus === 'PASSING' ? '✅'
|
|
: statusBlock.testsStatus === 'FAILING' ? '❌'
|
|
: '⏸';
|
|
|
|
// Build work type icon
|
|
const workIcon = statusBlock.workType === 'IMPLEMENTATION' ? '🔧'
|
|
: statusBlock.workType === 'TESTING' ? '🧪'
|
|
: statusBlock.workType === 'DOCUMENTATION' ? '📝'
|
|
: statusBlock.workType === 'REFACTORING' ? '♻️' : '📋';
|
|
|
|
let html = `
|
|
<div class="ralph-status-block-header">
|
|
<span>RALPH_STATUS</span>
|
|
<span class="ralph-status-block-status ${statusClass}">${escapeHtml(statusBlock.status)}</span>
|
|
${statusBlock.exitSignal ? '<span style="color: #4caf50;">🚪 EXIT</span>' : ''}
|
|
</div>
|
|
<div class="ralph-status-block-stats">
|
|
<span>${workIcon} ${escapeHtml(statusBlock.workType)}</span>
|
|
<span>📁 ${statusBlock.filesModified} files</span>
|
|
<span>✓ ${escapeHtml(String(statusBlock.tasksCompletedThisLoop))} tasks</span>
|
|
<span>${testsIcon} Tests: ${escapeHtml(statusBlock.testsStatus)}</span>
|
|
</div>
|
|
`;
|
|
|
|
if (statusBlock.recommendation) {
|
|
html += `<div class="ralph-status-block-recommendation">${escapeHtml(statusBlock.recommendation)}</div>`;
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
},
|
|
|
|
renderRalphTasks(todos) {
|
|
const grid = this.$('ralphTasksGrid');
|
|
if (!grid) return;
|
|
|
|
if (todos.length === 0) {
|
|
if (grid.children.length !== 1 || !grid.querySelector('.ralph-state-empty')) {
|
|
grid.innerHTML = '<div class="ralph-state-empty">No tasks detected</div>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Sort: by priority (P0 > P1 > P2 > null), then by status (in_progress > pending > completed)
|
|
const priorityOrder = { 'P0': 0, 'P1': 1, 'P2': 2, null: 3 };
|
|
const statusOrder = { in_progress: 0, pending: 1, completed: 2 };
|
|
const sorted = [...todos].sort((a, b) => {
|
|
const priA = priorityOrder[a.priority] ?? 3;
|
|
const priB = priorityOrder[b.priority] ?? 3;
|
|
if (priA !== priB) return priA - priB;
|
|
return (statusOrder[a.status] || 1) - (statusOrder[b.status] || 1);
|
|
});
|
|
|
|
// Always do full rebuild for enhanced features
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
sorted.forEach((todo, idx) => {
|
|
const card = this.createRalphTaskCard(todo, idx);
|
|
fragment.appendChild(card);
|
|
});
|
|
|
|
grid.innerHTML = '';
|
|
grid.appendChild(fragment);
|
|
},
|
|
|
|
createRalphTaskCard(todo, index) {
|
|
const card = document.createElement('div');
|
|
const statusClass = `task-${todo.status.replace('_', '-')}`;
|
|
const priorityClass = todo.priority ? `task-priority-${todo.priority.toLowerCase()}` : '';
|
|
card.className = `ralph-task-card ${statusClass} ${priorityClass}`.trim();
|
|
card.dataset.taskId = todo.id || index;
|
|
|
|
// Status icon
|
|
const iconSpan = document.createElement('span');
|
|
iconSpan.className = 'ralph-task-icon';
|
|
iconSpan.textContent = this.getRalphTaskIcon(todo.status);
|
|
card.appendChild(iconSpan);
|
|
|
|
// Priority badge if present
|
|
if (todo.priority) {
|
|
const prioritySpan = document.createElement('span');
|
|
prioritySpan.className = `ralph-task-priority priority-${todo.priority.toLowerCase()}`;
|
|
prioritySpan.textContent = todo.priority;
|
|
card.appendChild(prioritySpan);
|
|
}
|
|
|
|
// Task content
|
|
const contentSpan = document.createElement('span');
|
|
contentSpan.className = 'ralph-task-content';
|
|
contentSpan.textContent = todo.content;
|
|
card.appendChild(contentSpan);
|
|
|
|
// Attempts indicator (if > 0)
|
|
if (todo.attempts && todo.attempts > 0) {
|
|
const attemptsSpan = document.createElement('span');
|
|
attemptsSpan.className = 'ralph-task-attempts';
|
|
if (todo.lastError) {
|
|
attemptsSpan.classList.add('has-errors');
|
|
attemptsSpan.title = `Last error: ${todo.lastError}`;
|
|
}
|
|
attemptsSpan.textContent = `#${todo.attempts}`;
|
|
card.appendChild(attemptsSpan);
|
|
}
|
|
|
|
// Verification badge (if has verification criteria)
|
|
if (todo.verificationCriteria) {
|
|
const verifySpan = document.createElement('span');
|
|
verifySpan.className = 'ralph-task-verify-badge';
|
|
verifySpan.title = `Verify: ${todo.verificationCriteria}`;
|
|
verifySpan.textContent = '✓';
|
|
card.appendChild(verifySpan);
|
|
}
|
|
|
|
// Dependencies indicator
|
|
if (todo.dependencies && todo.dependencies.length > 0) {
|
|
const depsSpan = document.createElement('span');
|
|
depsSpan.className = 'ralph-task-deps-indicator';
|
|
depsSpan.title = `Depends on: ${todo.dependencies.join(', ')}`;
|
|
depsSpan.textContent = `↗${todo.dependencies.length}`;
|
|
card.appendChild(depsSpan);
|
|
}
|
|
|
|
// Quick action buttons (shown on hover)
|
|
const actions = document.createElement('div');
|
|
actions.className = 'ralph-task-actions';
|
|
|
|
if (todo.status !== 'completed') {
|
|
const completeBtn = document.createElement('button');
|
|
completeBtn.className = 'ralph-task-action-btn';
|
|
completeBtn.textContent = '✓';
|
|
completeBtn.title = 'Mark complete';
|
|
completeBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.updateRalphTaskStatus(todo.id, 'completed');
|
|
};
|
|
actions.appendChild(completeBtn);
|
|
}
|
|
|
|
if (todo.status === 'completed') {
|
|
const reopenBtn = document.createElement('button');
|
|
reopenBtn.className = 'ralph-task-action-btn';
|
|
reopenBtn.textContent = '↺';
|
|
reopenBtn.title = 'Reopen';
|
|
reopenBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.updateRalphTaskStatus(todo.id, 'pending');
|
|
};
|
|
actions.appendChild(reopenBtn);
|
|
}
|
|
|
|
if (todo.lastError) {
|
|
const retryBtn = document.createElement('button');
|
|
retryBtn.className = 'ralph-task-action-btn';
|
|
retryBtn.textContent = '↻';
|
|
retryBtn.title = 'Retry (clear error)';
|
|
retryBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.retryRalphTask(todo.id);
|
|
};
|
|
actions.appendChild(retryBtn);
|
|
}
|
|
|
|
card.appendChild(actions);
|
|
|
|
return card;
|
|
},
|
|
|
|
// Update a Ralph task's status via API
|
|
async updateRalphTaskStatus(taskId, newStatus) {
|
|
if (!this.activeSessionId) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/sessions/${this.activeSessionId}/plan/task/${taskId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status: newStatus })
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
throw new Error(data.error || 'Failed to update task');
|
|
}
|
|
|
|
this.showToast(`Task ${newStatus === 'completed' ? 'completed' : 'reopened'}`, 'success');
|
|
} catch (err) {
|
|
this.showToast('Failed to update task: ' + err.message, 'error');
|
|
}
|
|
},
|
|
|
|
// Retry a failed Ralph task (clear error, reset attempts)
|
|
async retryRalphTask(taskId) {
|
|
if (!this.activeSessionId) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/sessions/${this.activeSessionId}/plan/task/${taskId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ attempts: 0, lastError: null, status: 'pending' })
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
throw new Error(data.error || 'Failed to retry task');
|
|
}
|
|
|
|
this.showToast('Task reset for retry', 'success');
|
|
} catch (err) {
|
|
this.showToast('Failed to retry task: ' + err.message, 'error');
|
|
}
|
|
},
|
|
|
|
getRalphTaskIcon(status) {
|
|
switch (status) {
|
|
case 'completed': return '✓';
|
|
case 'in_progress': return '◐';
|
|
case 'pending':
|
|
default: return '○';
|
|
}
|
|
},
|
|
|
|
// Legacy method for backwards compatibility
|
|
getTodoIcon(status) {
|
|
return this.getRalphTaskIcon(status);
|
|
},
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Plan Versioning
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
// Update the plan version display in the Ralph panel
|
|
updatePlanVersionDisplay(version, historyLength) {
|
|
const versionRow = this.$('ralphVersionRow');
|
|
const versionBadge = this.$('ralphPlanVersion');
|
|
const rollbackBtn = this.$('ralphRollbackBtn');
|
|
|
|
if (!versionRow) return;
|
|
|
|
if (version && version > 0) {
|
|
versionRow.style.display = '';
|
|
if (versionBadge) versionBadge.textContent = `v${version}`;
|
|
if (rollbackBtn) {
|
|
rollbackBtn.style.display = historyLength > 1 ? '' : 'none';
|
|
}
|
|
} else {
|
|
versionRow.style.display = 'none';
|
|
}
|
|
},
|
|
|
|
// Show plan history dropdown
|
|
async showPlanHistory() {
|
|
if (!this.activeSessionId) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/sessions/${this.activeSessionId}/plan/history`);
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
this.showToast('Failed to load plan history: ' + data.error, 'error');
|
|
return;
|
|
}
|
|
|
|
const history = data.history || [];
|
|
if (history.length === 0) {
|
|
this.showToast('No plan history available', 'info');
|
|
return;
|
|
}
|
|
|
|
// Show history dropdown modal
|
|
this.showPlanHistoryModal(history, data.currentVersion);
|
|
} catch (err) {
|
|
this.showToast('Failed to load plan history: ' + err.message, 'error');
|
|
}
|
|
},
|
|
|
|
// Show the plan history modal
|
|
showPlanHistoryModal(history, currentVersion) {
|
|
// Remove existing modal if present
|
|
const existing = document.getElementById('planHistoryModal');
|
|
if (existing) existing.remove();
|
|
|
|
const modal = document.createElement('div');
|
|
modal.id = 'planHistoryModal';
|
|
modal.className = 'modal active';
|
|
modal.innerHTML = `
|
|
<div class="modal-backdrop" onclick="app.closePlanHistoryModal()"></div>
|
|
<div class="modal-content modal-sm">
|
|
<div class="modal-header">
|
|
<h3>Plan Version History</h3>
|
|
<button class="modal-close" onclick="app.closePlanHistoryModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.75rem;">
|
|
Current version: <strong>v${currentVersion}</strong>
|
|
</p>
|
|
<div class="plan-history-list">
|
|
${history.map(item => `
|
|
<div class="plan-history-item ${item.version === currentVersion ? 'current' : ''}"
|
|
onclick="app.rollbackToPlanVersion(${item.version})">
|
|
<div>
|
|
<span class="plan-history-version">v${item.version}</span>
|
|
<span class="plan-history-tasks">${item.taskCount || 0} tasks</span>
|
|
</div>
|
|
<span class="plan-history-time">${this.formatRelativeTime(item.timestamp)}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-toolbar" onclick="app.closePlanHistoryModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
},
|
|
|
|
closePlanHistoryModal() {
|
|
const modal = document.getElementById('planHistoryModal');
|
|
if (modal) modal.remove();
|
|
},
|
|
|
|
// Rollback to a specific plan version
|
|
async rollbackToPlanVersion(version) {
|
|
if (!this.activeSessionId) return;
|
|
|
|
if (!confirm(`Rollback to plan version ${version}? Current changes will be preserved in history.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/sessions/${this.activeSessionId}/plan/rollback/${version}`, {
|
|
method: 'POST'
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
this.showToast('Failed to rollback: ' + data.error, 'error');
|
|
return;
|
|
}
|
|
|
|
this.showToast(`Rolled back to plan v${version}`, 'success');
|
|
this.closePlanHistoryModal();
|
|
|
|
// Refresh the plan display
|
|
this.renderRalphStatePanel();
|
|
} catch (err) {
|
|
this.showToast('Failed to rollback: ' + err.message, 'error');
|
|
}
|
|
},
|
|
|
|
// Format relative time (e.g., "2 mins ago", "1 hour ago")
|
|
formatRelativeTime(timestamp) {
|
|
if (!timestamp) return '';
|
|
|
|
const now = Date.now();
|
|
const diff = now - timestamp;
|
|
|
|
const mins = Math.floor(diff / 60000);
|
|
const hours = Math.floor(diff / 3600000);
|
|
const days = Math.floor(diff / 86400000);
|
|
|
|
if (mins < 1) return 'just now';
|
|
if (mins < 60) return `${mins}m ago`;
|
|
if (hours < 24) return `${hours}h ago`;
|
|
return `${days}d ago`;
|
|
},
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Plan Wizard Agents in Monitor
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
renderMonitorPlanAgents() {
|
|
const section = document.getElementById('monitorPlanAgentsSection');
|
|
const body = document.getElementById('monitorPlanAgentsBody');
|
|
const stats = document.getElementById('monitorPlanAgentStats');
|
|
if (!section || !body) return;
|
|
|
|
const planAgents = Array.from(this.planSubagents?.values() || []);
|
|
const hasActiveOrchestrator = !!this.activePlanOrchestratorId;
|
|
|
|
// Show section only if there are plan agents or active orchestrator
|
|
if (planAgents.length === 0 && !hasActiveOrchestrator) {
|
|
section.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
section.style.display = '';
|
|
|
|
const activeCount = planAgents.filter(a => a.status === 'running').length;
|
|
const completedCount = planAgents.filter(a => a.status === 'completed' || a.status === 'failed').length;
|
|
|
|
if (stats) {
|
|
if (hasActiveOrchestrator) {
|
|
stats.textContent = `${activeCount} running, ${completedCount} done`;
|
|
} else {
|
|
stats.textContent = `${planAgents.length} total`;
|
|
}
|
|
}
|
|
|
|
if (planAgents.length === 0) {
|
|
body.innerHTML = `<div class="monitor-empty">${hasActiveOrchestrator ? 'Plan generation starting...' : 'No plan agents'}</div>`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
for (const agent of planAgents) {
|
|
const statusClass = agent.status === 'running' ? 'active' : agent.status === 'completed' ? 'completed' : 'error';
|
|
const agentLabel = agent.agentType || agent.agentId;
|
|
const modelBadge = agent.model ? `<span class="model-badge opus">opus</span>` : '';
|
|
const detail = agent.detail ? escapeHtml(agent.detail.substring(0, 50)) : '';
|
|
const duration = agent.durationMs ? `${(agent.durationMs / 1000).toFixed(1)}s` : '';
|
|
const itemCount = agent.itemCount ? `${agent.itemCount} items` : '';
|
|
|
|
html += `
|
|
<div class="process-item">
|
|
<span class="process-mode ${statusClass}">${agent.status || 'pending'}</span>
|
|
<div class="process-info">
|
|
<div class="process-name">${modelBadge} ${escapeHtml(agentLabel)}</div>
|
|
<div class="process-meta">
|
|
${detail ? `<span>${detail}</span>` : ''}
|
|
${itemCount ? `<span>${itemCount}</span>` : ''}
|
|
${duration ? `<span>${duration}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
body.innerHTML = html;
|
|
},
|
|
|
|
async cancelPlanFromMonitor() {
|
|
if (!this.activePlanOrchestratorId && this.planSubagents?.size === 0) {
|
|
this.showToast('No active plan generation', 'info');
|
|
return;
|
|
}
|
|
|
|
if (!confirm('Cancel plan generation and close all plan agent windows?')) return;
|
|
|
|
// Cancel the plan generation (reuse existing method)
|
|
await this.cancelPlanGeneration();
|
|
|
|
// Also force close the wizard if it's open
|
|
const wizardModal = document.getElementById('ralphWizardModal');
|
|
if (wizardModal?.classList.contains('active')) {
|
|
this.closeRalphWizard();
|
|
}
|
|
|
|
// Update monitor display
|
|
this.renderMonitorPlanAgents();
|
|
this.showToast('Plan generation cancelled', 'success');
|
|
},
|
|
});
|