chore: bump version to 0.1410

This commit is contained in:
arkon
2026-01-28 04:04:40 +01:00
parent beb4242c74
commit 38c36502ec
6 changed files with 1212 additions and 13 deletions
+714 -8
View File
@@ -465,9 +465,15 @@ class ClaudemanApp {
this.ralphStatePanelCollapsed = true; // Default to collapsed
// Plan subagent windows (visible Opus agents during plan generation)
this.planSubagents = new Map(); // Map<agentId, { type, model, status, startTime, element }>
this.planSubagents = new Map(); // Map<agentId, { type, model, status, startTime, element, relativePos }>
this.planSubagentWindowZIndex = 1100;
this.planGenerationStopped = false; // Flag to ignore SSE events after Stop
this.planAgentsMinimized = false; // Whether agent windows are minimized to tab
// Wizard dragging state
this.wizardDragState = null; // { startX, startY, startLeft, startTop, isDragging }
this.wizardDragListeners = null; // { move, up } for cleanup
this.wizardPosition = null; // { left, top } - null means centered
// Project Insights tracking (active Bash tools with clickable file paths)
this.projectInsights = new Map(); // Map<sessionId, ActiveBashTool[]>
@@ -2672,7 +2678,25 @@ class ClaudemanApp {
// Show wizard modal
this.updateRalphWizardUI();
document.getElementById('ralphWizardModal').classList.add('active');
const modal = document.getElementById('ralphWizardModal');
modal.classList.add('active');
// Reset wizard position to center
this.wizardPosition = null;
this.planAgentsMinimized = false;
const wizardContent = modal.querySelector('.modal-content');
if (wizardContent) {
wizardContent.classList.remove('draggable');
wizardContent.style.left = '';
wizardContent.style.top = '';
}
// Set up wizard dragging
this.setupWizardDragging();
// Hide minimize button initially (no agents yet)
this.updateWizardAgentCount();
document.getElementById('ralphTaskDescription').focus();
// Update connection lines to show wizard connections
@@ -2680,7 +2704,18 @@ class ClaudemanApp {
}
closeRalphWizard() {
document.getElementById('ralphWizardModal')?.classList.remove('active');
const modal = document.getElementById('ralphWizardModal');
modal?.classList.remove('active');
// Clean up wizard drag listeners
this.cleanupWizardDragging();
// Remove agents minimized tab if exists
this.removeAgentsMinimizedTab();
// Reset wizard position
this.wizardPosition = null;
this.planAgentsMinimized = false;
// Update connection lines (wizard closed, revert to normal)
this.updateConnectionLines();
@@ -3882,8 +3917,10 @@ class ClaudemanApp {
win.innerHTML = `
<div class="plan-subagent-header">
<span class="plan-subagent-icon">${typeIcons[agentType] || '🤖'}</span>
<span class="plan-subagent-title">${typeLabels[agentType] || agentType}</span>
<span class="plan-subagent-prompt-link" data-agent-id="${agentId}" data-agent-type="${agentType}" title="Click to view prompt">
<span class="plan-subagent-icon">${typeIcons[agentType] || '🤖'}</span>
<span class="plan-subagent-title">${typeLabels[agentType] || agentType}</span>
</span>
<span class="plan-subagent-model">${model}</span>
</div>
<div class="plan-subagent-body">
@@ -3895,21 +3932,104 @@ class ClaudemanApp {
</div>
`;
// Add click handler for prompt link
const promptLink = win.querySelector('.plan-subagent-prompt-link');
promptLink.addEventListener('click', (e) => {
e.stopPropagation();
this.showPlanAgentPrompt(agentId, agentType);
});
document.body.appendChild(win);
// Store reference
// Calculate relative position to wizard
let relativePos = null;
if (wizardRect) {
relativePos = {
dx: x - wizardRect.left,
dy: y - wizardRect.top,
};
}
// Make the window draggable
const header = win.querySelector('.plan-subagent-header');
const dragListeners = this.makePlanSubagentDraggable(win, header);
// Store reference with relative position and drag listeners
this.planSubagents.set(agentId, {
agentId,
type: agentType,
model,
status: 'running',
startTime: Date.now(),
element: win,
relativePos,
dragListeners,
});
// If agents are minimized, hide this new window too
if (this.planAgentsMinimized) {
win.style.display = 'none';
}
// Update wizard agent count button
this.updateWizardAgentCount();
// Update connection lines
this.updateConnectionLines();
}
makePlanSubagentDraggable(win, handle) {
let isDragging = false;
let startX, startY, startLeft, startTop;
let dragUpdateScheduled = false;
const mousedownHandler = (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(win.style.left) || 0;
startTop = parseInt(win.style.top) || 0;
win.style.zIndex = ++this.planSubagentWindowZIndex;
e.preventDefault();
};
const moveHandler = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
// Constrain to viewport
const winWidth = win.offsetWidth || 280;
const winHeight = win.offsetHeight || 100;
const maxX = window.innerWidth - winWidth - 10;
const maxY = window.innerHeight - winHeight - 10;
const newLeft = Math.max(10, Math.min(startLeft + dx, maxX));
const newTop = Math.max(10, Math.min(startTop + dy, maxY));
win.style.left = `${newLeft}px`;
win.style.top = `${newTop}px`;
// Throttle connection line updates
if (!dragUpdateScheduled) {
dragUpdateScheduled = true;
requestAnimationFrame(() => {
this.updateConnectionLines();
dragUpdateScheduled = false;
});
}
};
const upHandler = () => {
isDragging = false;
};
handle.addEventListener('mousedown', mousedownHandler);
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
return { mousedown: mousedownHandler, move: moveHandler, up: upHandler };
}
updatePlanSubagentWindow(agentId, status, itemCount, durationMs, error) {
const windowData = this.planSubagents.get(agentId);
if (!windowData?.element) return;
@@ -3921,12 +4041,21 @@ class ClaudemanApp {
const detailEl = win.querySelector('.plan-subagent-detail');
windowData.status = status;
windowData.itemCount = itemCount;
if (status === 'completed') {
statusEl?.classList.remove('running');
statusEl?.classList.add('completed');
if (spinnerEl) spinnerEl.style.display = 'none';
if (statusTextEl) statusTextEl.textContent = `Done (${itemCount || 0} items)`;
if (statusTextEl) {
statusTextEl.textContent = `Done (${itemCount || 0} items)`;
statusTextEl.classList.add('clickable');
statusTextEl.title = 'Click to view result files';
statusTextEl.onclick = (e) => {
e.stopPropagation();
this.showPlanAgentFiles(windowData.agentId, windowData.type);
};
}
if (detailEl && durationMs) detailEl.textContent = `${(durationMs / 1000).toFixed(1)}s`;
} else if (status === 'failed' || status === 'cancelled') {
statusEl?.classList.remove('running');
@@ -3936,20 +4065,596 @@ class ClaudemanApp {
if (detailEl) detailEl.textContent = error || '';
}
// Update minimized tab if agents are hidden
if (this.planAgentsMinimized) {
this.createAgentsMinimizedTab();
}
// Update connection lines
this.updateConnectionLines();
}
closePlanSubagentWindows() {
for (const [agentId, windowData] of this.planSubagents) {
// Clean up drag listeners
if (windowData.dragListeners) {
document.removeEventListener('mousemove', windowData.dragListeners.move);
document.removeEventListener('mouseup', windowData.dragListeners.up);
}
if (windowData.element) {
windowData.element.remove();
}
}
this.planSubagents.clear();
this.updateWizardAgentCount();
this.removeAgentsMinimizedTab();
this.updateConnectionLines();
}
// ========== Wizard Dragging ==========
setupWizardDragging() {
const modal = document.getElementById('ralphWizardModal');
const wizardContent = modal?.querySelector('.modal-content');
const header = wizardContent?.querySelector('.modal-header');
if (!wizardContent || !header) return;
// Clean up any existing listeners
this.cleanupWizardDragging();
let isDragging = false;
let startX, startY, startLeft, startTop;
let dragUpdateScheduled = false;
let agentStartPositions = new Map(); // Track agent window start positions during drag
const mousedownHandler = (e) => {
// Don't drag if clicking buttons
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return;
isDragging = true;
// If wizard hasn't been moved yet, calculate its current centered position
if (!this.wizardPosition) {
const rect = wizardContent.getBoundingClientRect();
this.wizardPosition = { left: rect.left, top: rect.top };
}
startX = e.clientX;
startY = e.clientY;
startLeft = this.wizardPosition.left;
startTop = this.wizardPosition.top;
// Capture agent window start positions
agentStartPositions.clear();
for (const [agentId, windowData] of this.planSubagents) {
if (windowData.element) {
agentStartPositions.set(agentId, {
left: parseInt(windowData.element.style.left) || 0,
top: parseInt(windowData.element.style.top) || 0,
});
}
}
// Switch to absolute positioning
wizardContent.classList.add('draggable');
wizardContent.style.left = `${startLeft}px`;
wizardContent.style.top = `${startTop}px`;
e.preventDefault();
};
const moveHandler = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
// Constrain to viewport
const winWidth = wizardContent.offsetWidth || 480;
const winHeight = wizardContent.offsetHeight || 400;
const maxX = window.innerWidth - winWidth - 10;
const maxY = window.innerHeight - winHeight - 10;
const newLeft = Math.max(10, Math.min(startLeft + dx, maxX));
const newTop = Math.max(10, Math.min(startTop + dy, maxY));
this.wizardPosition = { left: newLeft, top: newTop };
wizardContent.style.left = `${newLeft}px`;
wizardContent.style.top = `${newTop}px`;
// Move agent windows using their start positions + delta
this.moveAgentWindowsWithWizard(agentStartPositions, dx, dy);
// Throttle connection line updates
if (!dragUpdateScheduled) {
dragUpdateScheduled = true;
requestAnimationFrame(() => {
this.updateConnectionLines();
dragUpdateScheduled = false;
});
}
};
const upHandler = () => {
if (isDragging) {
isDragging = false;
// Save agent window positions for next drag
this.saveAgentWindowRelativePositions();
}
};
header.addEventListener('mousedown', mousedownHandler);
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
this.wizardDragListeners = {
header,
mousedown: mousedownHandler,
move: moveHandler,
up: upHandler,
};
}
cleanupWizardDragging() {
if (this.wizardDragListeners) {
const { header, mousedown, move, up } = this.wizardDragListeners;
header?.removeEventListener('mousedown', mousedown);
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
this.wizardDragListeners = null;
}
}
moveAgentWindowsWithWizard(agentStartPositions, dx, dy) {
// Move plan subagent windows using their start positions + delta
for (const [agentId, windowData] of this.planSubagents) {
if (windowData.element && !this.planAgentsMinimized) {
const startPos = agentStartPositions.get(agentId);
if (startPos) {
windowData.element.style.left = `${startPos.left + dx}px`;
windowData.element.style.top = `${startPos.top + dy}px`;
}
}
}
}
saveAgentWindowRelativePositions() {
const wizardModal = document.getElementById('ralphWizardModal');
const wizardContent = wizardModal?.querySelector('.modal-content');
if (!wizardContent) return;
const wizardRect = wizardContent.getBoundingClientRect();
for (const [, windowData] of this.planSubagents) {
if (windowData.element) {
const winRect = windowData.element.getBoundingClientRect();
windowData.relativePos = {
dx: winRect.left - wizardRect.left,
dy: winRect.top - wizardRect.top,
};
}
}
}
// ========== Agent Windows Minimize/Expand ==========
togglePlanAgentsMinimized() {
this.planAgentsMinimized = !this.planAgentsMinimized;
if (this.planAgentsMinimized) {
// Hide all agent windows and show tab
for (const [, windowData] of this.planSubagents) {
if (windowData.element) {
windowData.element.style.display = 'none';
}
}
this.createAgentsMinimizedTab();
} else {
// Show all agent windows and remove tab
for (const [, windowData] of this.planSubagents) {
if (windowData.element) {
windowData.element.style.display = '';
}
}
this.removeAgentsMinimizedTab();
}
this.updateConnectionLines();
}
createAgentsMinimizedTab() {
// Remove existing tab if any
this.removeAgentsMinimizedTab();
const runningCount = Array.from(this.planSubagents.values())
.filter(w => w.status === 'running').length;
const totalCount = this.planSubagents.size;
const tab = document.createElement('div');
tab.className = 'wizard-agents-tab';
tab.id = 'wizardAgentsMinimizedTab';
tab.onclick = () => this.togglePlanAgentsMinimized();
tab.innerHTML = `
<span class="tab-icon">🤖</span>
<span class="tab-label">Plan Agents</span>
<span class="tab-count">${totalCount}</span>
${runningCount > 0 ? `
<span class="tab-running">
<span class="tab-spinner"></span>
${runningCount} running
</span>
` : ''}
`;
document.body.appendChild(tab);
}
removeAgentsMinimizedTab() {
document.getElementById('wizardAgentsMinimizedTab')?.remove();
}
updateWizardAgentCount() {
const btn = document.getElementById('wizardMinimizeAgentsBtn');
const countEl = document.getElementById('wizardAgentCount');
if (!btn || !countEl) return;
const count = this.planSubagents.size;
countEl.textContent = count;
btn.style.display = count > 0 ? '' : 'none';
// Update minimized tab if it exists
if (this.planAgentsMinimized) {
this.createAgentsMinimizedTab();
}
}
// ========== Plan Agent Prompts & Files Viewer ==========
async showPlanAgentPrompt(agentId, agentType) {
const caseName = this.ralphWizardConfig?.caseName;
if (!caseName) {
console.error('No case name available');
return;
}
try {
const filePath = `${agentType}/prompt.md`;
const res = await fetch(`/api/cases/${encodeURIComponent(caseName)}/ralph-wizard/file/${encodeURIComponent(filePath)}`);
const data = await res.json();
if (!data.success) {
this.showPlanFileWindow(agentType, 'prompt.md', `Error: ${data.error}`, false);
return;
}
this.showPlanFileWindow(agentType, 'prompt.md', data.data.content, false);
} catch (err) {
console.error('Failed to fetch prompt:', err);
this.showPlanFileWindow(agentType, 'prompt.md', `Error fetching prompt: ${err.message}`, false);
}
}
async showPlanAgentFiles(agentId, agentType) {
const caseName = this.ralphWizardConfig?.caseName;
if (!caseName) {
console.error('No case name available');
return;
}
// Create file manager popup
this.showPlanFileManager(agentType, caseName);
}
showPlanFileManager(agentType, caseName) {
// Remove existing file manager if any
document.getElementById('planFileManager')?.remove();
const typeLabels = {
research: 'Research Agent',
requirements: 'Requirements Analyst',
architecture: 'Architecture Planner',
testing: 'TDD Specialist',
risks: 'Risk Analyst',
verification: 'Verification Expert',
execution: 'Execution Optimizer',
'final-review': 'Final Review',
};
const fm = document.createElement('div');
fm.className = 'plan-file-manager';
fm.id = 'planFileManager';
fm.innerHTML = `
<div class="plan-fm-header">
<span class="plan-fm-title">${typeLabels[agentType] || agentType} Files</span>
<button class="plan-fm-close" onclick="document.getElementById('planFileManager')?.remove()">&times;</button>
</div>
<div class="plan-fm-body">
<div class="plan-fm-loading">Loading files...</div>
</div>
`;
document.body.appendChild(fm);
// Make draggable
this.makePlanFileManagerDraggable(fm);
// Fetch files
this.loadPlanFileManagerFiles(agentType, caseName, fm);
}
async loadPlanFileManagerFiles(agentType, caseName, fm) {
const body = fm.querySelector('.plan-fm-body');
try {
// First try to get the result.json
const resultPath = `${agentType}/result.json`;
const res = await fetch(`/api/cases/${encodeURIComponent(caseName)}/ralph-wizard/file/${encodeURIComponent(resultPath)}`);
const data = await res.json();
if (!data.success) {
body.innerHTML = `<div class="plan-fm-error">No result file found</div>`;
return;
}
const parsed = data.data.parsed;
let items = [];
// Extract items based on agent type
if (agentType === 'research' && parsed?.findings) {
// Research has nested structure
items = [
{ name: 'External Resources', type: 'section', count: parsed.findings.externalResources?.length || 0 },
{ name: 'Codebase Patterns', type: 'section', count: parsed.findings.codebasePatterns?.length || 0 },
{ name: 'Technical Recommendations', type: 'section', count: parsed.findings.technicalRecommendations?.length || 0 },
{ name: 'Potential Challenges', type: 'section', count: parsed.findings.potentialChallenges?.length || 0 },
{ name: 'Recommended Tools', type: 'section', count: parsed.findings.recommendedTools?.length || 0 },
];
} else if (parsed?.items) {
items = parsed.items.map((item, idx) => ({
name: item.category || item.content?.substring(0, 50) || `Item ${idx + 1}`,
type: 'item',
content: item.content || JSON.stringify(item, null, 2),
rationale: item.rationale,
}));
} else {
// Fallback: show raw JSON structure
items = Object.keys(parsed || {}).map(key => ({
name: key,
type: 'key',
content: JSON.stringify(parsed[key], null, 2),
}));
}
body.innerHTML = `
<div class="plan-fm-files">
<div class="plan-fm-file" data-file="prompt.md">
<span class="plan-fm-file-icon">📄</span>
<span class="plan-fm-file-name">prompt.md</span>
</div>
<div class="plan-fm-file" data-file="result.json">
<span class="plan-fm-file-icon">📊</span>
<span class="plan-fm-file-name">result.json</span>
</div>
<div class="plan-fm-divider"></div>
<div class="plan-fm-section-title">Result Contents (${items.length} items)</div>
${items.map((item, idx) => `
<div class="plan-fm-item" data-item-idx="${idx}" data-agent-type="${agentType}">
<span class="plan-fm-item-icon">${item.type === 'section' ? '📁' : '📝'}</span>
<span class="plan-fm-item-name">${this.escapeHtml(item.name)}</span>
${item.count !== undefined ? `<span class="plan-fm-item-count">${item.count}</span>` : ''}
</div>
`).join('')}
</div>
`;
// Store items data for click handlers
fm._itemsData = items;
fm._parsedData = parsed;
fm._caseName = caseName;
fm._agentType = agentType;
// Add click handlers
body.querySelectorAll('.plan-fm-file').forEach(el => {
el.onclick = () => this.openPlanFile(agentType, caseName, el.dataset.file);
});
body.querySelectorAll('.plan-fm-item').forEach(el => {
el.onclick = () => this.openPlanFileItem(fm, parseInt(el.dataset.itemIdx));
});
} catch (err) {
body.innerHTML = `<div class="plan-fm-error">Error: ${err.message}</div>`;
}
}
async openPlanFile(agentType, caseName, fileName) {
const filePath = `${agentType}/${fileName}`;
try {
const res = await fetch(`/api/cases/${encodeURIComponent(caseName)}/ralph-wizard/file/${encodeURIComponent(filePath)}`);
const data = await res.json();
if (!data.success) {
this.showPlanFileWindow(agentType, fileName, `Error: ${data.error}`, fileName.endsWith('.json'));
return;
}
const content = fileName.endsWith('.json')
? JSON.stringify(data.data.parsed, null, 2)
: data.data.content;
this.showPlanFileWindow(agentType, fileName, content, fileName.endsWith('.json'));
} catch (err) {
this.showPlanFileWindow(agentType, fileName, `Error: ${err.message}`, false);
}
}
openPlanFileItem(fm, itemIdx) {
const items = fm._itemsData;
const parsed = fm._parsedData;
const agentType = fm._agentType;
if (!items || itemIdx >= items.length) return;
const item = items[itemIdx];
// For research agent, expand the section
if (agentType === 'research' && item.type === 'section') {
let sectionData;
switch (item.name) {
case 'External Resources':
sectionData = parsed.findings?.externalResources;
break;
case 'Codebase Patterns':
sectionData = parsed.findings?.codebasePatterns;
break;
case 'Technical Recommendations':
sectionData = parsed.findings?.technicalRecommendations;
break;
case 'Potential Challenges':
sectionData = parsed.findings?.potentialChallenges;
break;
case 'Recommended Tools':
sectionData = parsed.findings?.recommendedTools;
break;
}
const content = JSON.stringify(sectionData, null, 2);
this.showPlanFileWindow(agentType, item.name, content, true);
} else {
// Show item content
const content = item.content || JSON.stringify(item, null, 2);
this.showPlanFileWindow(agentType, item.name, content, typeof item.content !== 'string');
}
}
showPlanFileWindow(agentType, fileName, content, isJson) {
const windowId = `plan-file-${Date.now()}`;
const win = document.createElement('div');
win.className = 'plan-file-window';
win.id = windowId;
win.style.left = `${100 + Math.random() * 200}px`;
win.style.top = `${100 + Math.random() * 100}px`;
win.style.zIndex = ++this.planSubagentWindowZIndex;
const typeLabels = {
research: 'Research',
requirements: 'Requirements',
architecture: 'Architecture',
testing: 'Testing',
risks: 'Risks',
verification: 'Verification',
execution: 'Execution',
'final-review': 'Review',
};
win.innerHTML = `
<div class="plan-file-window-header">
<span class="plan-file-window-title">${typeLabels[agentType] || agentType} / ${this.escapeHtml(fileName)}</span>
<button class="plan-file-window-close" onclick="document.getElementById('${windowId}')?.remove()">&times;</button>
</div>
<div class="plan-file-window-body">
<pre class="plan-file-content ${isJson ? 'json' : 'markdown'}">${this.escapeHtml(content)}</pre>
</div>
<div class="plan-file-window-resize"></div>
`;
document.body.appendChild(win);
// Make draggable and resizable
this.makePlanFileWindowDraggable(win);
this.makePlanFileWindowResizable(win);
}
makePlanFileManagerDraggable(fm) {
const header = fm.querySelector('.plan-fm-header');
let isDragging = false;
let startX, startY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(fm.style.left) || (window.innerWidth / 2 - 150);
startTop = parseInt(fm.style.top) || (window.innerHeight / 2 - 200);
fm.style.left = `${startLeft}px`;
fm.style.top = `${startTop}px`;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
fm.style.left = `${Math.max(10, startLeft + dx)}px`;
fm.style.top = `${Math.max(10, startTop + dy)}px`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
makePlanFileWindowDraggable(win) {
const header = win.querySelector('.plan-file-window-header');
let isDragging = false;
let startX, startY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(win.style.left) || 0;
startTop = parseInt(win.style.top) || 0;
win.style.zIndex = ++this.planSubagentWindowZIndex;
e.preventDefault();
});
const moveHandler = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
win.style.left = `${Math.max(10, startLeft + dx)}px`;
win.style.top = `${Math.max(10, startTop + dy)}px`;
};
const upHandler = () => {
isDragging = false;
};
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
}
makePlanFileWindowResizable(win) {
const resizer = win.querySelector('.plan-file-window-resize');
let isResizing = false;
let startX, startY, startWidth, startHeight;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = win.offsetWidth;
startHeight = win.offsetHeight;
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
win.style.width = `${Math.max(300, startWidth + dx)}px`;
win.style.height = `${Math.max(200, startHeight + dy)}px`;
});
document.addEventListener('mouseup', () => {
isResizing = false;
});
}
skipPlanGeneration() {
// Stop any running timers
if (this.planLoadingTimer) {
@@ -8327,7 +9032,8 @@ class ClaudemanApp {
}
// Draw lines from wizard to plan subagent windows (Opus agents during plan generation)
if (wizardOpen && wizardContent && this.planSubagents.size > 0) {
// Skip if agents are minimized to tab
if (wizardOpen && wizardContent && this.planSubagents.size > 0 && !this.planAgentsMinimized) {
const wizardRect = wizardContent.getBoundingClientRect();
for (const [agentId, windowData] of this.planSubagents) {
+7 -1
View File
@@ -946,7 +946,13 @@
<div class="modal-content modal-wizard">
<div class="modal-header">
<h3>Start Ralph Loop</h3>
<button class="modal-close" onclick="app.closeRalphWizard()">&times;</button>
<div class="modal-header-actions">
<button class="wizard-minimize-agents" id="wizardMinimizeAgentsBtn" onclick="app.togglePlanAgentsMinimized()" style="display: none;">
<span class="icon">🤖</span>
<span class="agent-count" id="wizardAgentCount">0</span>
</button>
<button class="modal-close" onclick="app.closeRalphWizard()">&times;</button>
</div>
</div>
<!-- Wizard Progress Steps -->
+373
View File
@@ -2845,6 +2845,12 @@ body {
font-weight: 500;
}
.modal-header-actions {
display: flex;
align-items: center;
gap: 0.25rem;
}
.modal-close {
background: transparent;
border: none;
@@ -5524,6 +5530,111 @@ kbd {
flex-direction: column;
}
/* Draggable wizard state */
.modal-wizard.draggable {
position: absolute;
transform: none;
cursor: default;
}
.modal-wizard .modal-header {
cursor: grab;
user-select: none;
}
.modal-wizard .modal-header:active {
cursor: grabbing;
}
/* Agents minimized tab */
.wizard-agents-tab {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: 20px;
padding: 0.5rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 20px rgba(96, 165, 250, 0.2);
z-index: 1200;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.wizard-agents-tab:hover {
transform: translateX(-50%) scale(1.05);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.5), 0 0 25px rgba(96, 165, 250, 0.3);
}
.wizard-agents-tab .tab-icon {
font-size: 1.1rem;
}
.wizard-agents-tab .tab-label {
font-size: 0.85rem;
font-weight: 500;
color: var(--text);
}
.wizard-agents-tab .tab-count {
background: var(--accent);
color: white;
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
}
.wizard-agents-tab .tab-running {
display: flex;
align-items: center;
gap: 0.35rem;
}
.wizard-agents-tab .tab-spinner {
width: 12px;
height: 12px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Minimize button in wizard header */
.wizard-minimize-agents {
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
color: var(--text-dim);
cursor: pointer;
margin-right: 0.5rem;
display: flex;
align-items: center;
gap: 0.3rem;
transition: all 0.15s ease;
}
.wizard-minimize-agents:hover {
background: var(--bg-hover);
color: var(--text);
border-color: var(--accent);
}
.wizard-minimize-agents .agent-count {
background: var(--accent);
color: white;
padding: 0.1rem 0.35rem;
border-radius: 8px;
font-size: 0.65rem;
font-weight: 600;
}
.modal-wizard .modal-body {
overflow-y: auto;
flex: 1;
@@ -5886,6 +5997,12 @@ kbd {
padding: 0.5rem 0.75rem;
background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(96, 165, 250, 0.1));
border-bottom: 1px solid var(--border);
cursor: grab;
user-select: none;
}
.plan-subagent-header:active {
cursor: grabbing;
}
.plan-subagent-icon {
@@ -5953,6 +6070,262 @@ kbd {
text-overflow: ellipsis;
}
/* Clickable prompt link in plan subagent header */
.plan-subagent-prompt-link {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.15rem 0.3rem;
margin: -0.15rem -0.3rem;
border-radius: 4px;
transition: background 0.15s ease;
}
.plan-subagent-prompt-link:hover {
background: rgba(255, 255, 255, 0.1);
}
/* Clickable status text */
.plan-subagent-status-text.clickable {
cursor: pointer;
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 2px;
}
.plan-subagent-status-text.clickable:hover {
text-decoration-style: solid;
color: var(--accent);
}
/* ========== Plan File Manager ========== */
.plan-file-manager {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 320px;
max-height: 450px;
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 20px rgba(96, 165, 250, 0.15);
z-index: 2000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.plan-fm-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: linear-gradient(135deg, rgba(96, 165, 250, 0.15), rgba(96, 165, 250, 0.05));
border-bottom: 1px solid var(--border);
cursor: grab;
}
.plan-fm-header:active {
cursor: grabbing;
}
.plan-fm-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--text);
}
.plan-fm-close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.2rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
line-height: 1;
border-radius: 4px;
}
.plan-fm-close:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text);
}
.plan-fm-body {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.plan-fm-loading,
.plan-fm-error {
padding: 2rem;
text-align: center;
color: var(--text-muted);
}
.plan-fm-error {
color: var(--red);
}
.plan-fm-files {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.plan-fm-file,
.plan-fm-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s ease;
}
.plan-fm-file:hover,
.plan-fm-item:hover {
background: rgba(96, 165, 250, 0.15);
}
.plan-fm-file-icon,
.plan-fm-item-icon {
font-size: 1rem;
}
.plan-fm-file-name,
.plan-fm-item-name {
flex: 1;
font-size: 0.85rem;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.plan-fm-item-count {
background: var(--bg-dark);
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-size: 0.7rem;
color: var(--text-muted);
}
.plan-fm-divider {
height: 1px;
background: var(--border);
margin: 0.5rem 0;
}
.plan-fm-section-title {
font-size: 0.75rem;
color: var(--text-muted);
padding: 0.25rem 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ========== Plan File Viewer Window ========== */
.plan-file-window {
position: fixed;
width: 500px;
height: 400px;
min-width: 300px;
min-height: 200px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
z-index: 1500;
display: flex;
flex-direction: column;
overflow: hidden;
}
.plan-file-window-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: var(--bg-dark);
border-bottom: 1px solid var(--border);
cursor: grab;
flex-shrink: 0;
}
.plan-file-window-header:active {
cursor: grabbing;
}
.plan-file-window-title {
font-size: 0.8rem;
font-weight: 500;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.plan-file-window-close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.1rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
line-height: 1;
border-radius: 4px;
}
.plan-file-window-close:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text);
}
.plan-file-window-body {
flex: 1;
overflow: auto;
padding: 0;
}
.plan-file-content {
margin: 0;
padding: 1rem;
font-family: 'SF Mono', Monaco, 'Menlo', monospace;
font-size: 0.75rem;
line-height: 1.5;
color: var(--text);
white-space: pre-wrap;
word-break: break-word;
}
.plan-file-content.json {
color: #a5d6ff;
}
.plan-file-content.markdown {
color: var(--text-dim);
}
.plan-file-window-resize {
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, var(--border) 50%);
border-radius: 0 0 8px 0;
}
.plan-file-window-resize:hover {
background: linear-gradient(135deg, transparent 50%, var(--accent) 50%);
}
/* Note: main .plan-item styles are in "Enhanced Plan Item Styling" section below */
.plan-item-checkbox {
+90
View File
@@ -2561,6 +2561,96 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return { success: true, data: { cancelled } };
});
// Get ralph-wizard files for a case (prompts and results)
this.app.get('/api/cases/:caseName/ralph-wizard/files', async (req) => {
const { caseName } = req.params as { caseName: string };
const casesDir = join(homedir(), 'claudeman-cases');
const casePath = join(casesDir, caseName);
// Security: Path traversal protection
const resolvedCase = resolve(casePath);
const resolvedBase = resolve(casesDir);
if (!resolvedCase.startsWith(resolvedBase)) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
}
const wizardDir = join(casePath, 'ralph-wizard');
if (!existsSync(wizardDir)) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Ralph wizard directory not found');
}
// List all subdirectories and their files
const files: Array<{ agentType: string; promptFile?: string; resultFile?: string }> = [];
const entries = readdirSync(wizardDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const agentDir = join(wizardDir, entry.name);
const agentFiles: { agentType: string; promptFile?: string; resultFile?: string } = {
agentType: entry.name,
};
if (existsSync(join(agentDir, 'prompt.md'))) {
agentFiles.promptFile = `${entry.name}/prompt.md`;
}
if (existsSync(join(agentDir, 'result.json'))) {
agentFiles.resultFile = `${entry.name}/result.json`;
}
if (agentFiles.promptFile || agentFiles.resultFile) {
files.push(agentFiles);
}
}
}
return { success: true, data: { files, caseName } };
});
// Read a specific ralph-wizard file
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req) => {
const { caseName, filePath } = req.params as { caseName: string; filePath: string };
const casesDir = join(homedir(), 'claudeman-cases');
const casePath = join(casesDir, caseName);
// Security: Path traversal protection for case name
const resolvedCase = resolve(casePath);
const resolvedBase = resolve(casesDir);
if (!resolvedCase.startsWith(resolvedBase)) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
}
const wizardDir = join(casePath, 'ralph-wizard');
// Decode the file path (it may be URL encoded)
const decodedPath = decodeURIComponent(filePath);
const fullPath = join(wizardDir, decodedPath);
// Security: ensure path is within wizard directory
const resolvedPath = resolve(fullPath);
const resolvedWizard = resolve(wizardDir);
if (!resolvedPath.startsWith(resolvedWizard)) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid file path');
}
if (!existsSync(fullPath)) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found');
}
const content = readFileSync(fullPath, 'utf-8');
const isJson = filePath.endsWith('.json');
return {
success: true,
data: {
content,
filePath: decodedPath,
isJson,
parsed: isJson ? JSON.parse(content) : null,
},
};
});
// ============ Plan Management Endpoints ============
// These endpoints support runtime plan adaptation with checkpoints, failure tracking, and versioning