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
+27 -3
View File
@@ -16,7 +16,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md`
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1409 (must match `package.json`)
**Version**: 0.1410 (must match `package.json`)
## Project Overview
@@ -41,8 +41,9 @@ npm run typecheck # Type check
# Testing
npx vitest run # All tests
npx vitest run test/<file>.test.ts # Single file
npx vitest run -t "pattern" # Tests matching name
npm run test:coverage # With coverage report
npm run test:e2e # Browser E2E (run `npx playwright install chromium` first)
npm run test:e2e # Browser E2E (requires: npx playwright install chromium)
# Production
npm run build
@@ -107,7 +108,7 @@ journalctl --user -u claudeman-web -f
## Testing
**Port allocation**: E2E tests use centralized ports in `test/e2e/e2e.config.ts` (E2E_PORTS: 3183-3190). Unit/integration tests pick unique ports manually (3099-3157). Search `const PORT =` or `TEST_PORT` in test files to see used ports. **Next available: 3192**
**Port allocation**: E2E tests use centralized ports in `test/e2e/e2e.config.ts`. Unit/integration tests pick unique ports manually. Search `const PORT =` or `TEST_PORT` in test files to find used ports before adding new tests.
**E2E tests**: Use Playwright. Run `npx playwright install chromium` first. See `test/e2e/fixtures/` for helpers. E2E config provides ports, timeouts, and helpers.
@@ -165,3 +166,26 @@ The app must stay fast with 20 sessions and 50 agent windows:
| **Test fixtures** | `test/e2e/fixtures/` |
| **Test utilities** | `test/respawn-test-utils.ts` |
| **Keyboard shortcuts** | README.md or App Settings in web UI |
## Active Ralph Loop Task
**Current Task**: Create an improvement plan to make this app(Claudeman) significantly better, but focus on performance, memory leaks, that it runs really fluid and fast and all the functions are working properly also in edge cases.
**Case Folder**: `/home/arkon/claudeman-cases/claudeman`
### Key Files
- **Plan Summary**: `/home/arkon/claudeman-cases/claudeman/ralph-wizard/summary.md` - Human-readable plan overview
- **Todo Items**: `/home/arkon/claudeman-cases/claudeman/ralph-wizard/final-result.json` - Contains `items` array with all todo tasks
- **Research**: `/home/arkon/claudeman-cases/claudeman/ralph-wizard/research/result.json` - External resources and codebase patterns
### How to Work on This Task
1. Read the plan summary to understand the overall approach
2. Check `final-result.json` for the todo items array - each item has `id`, `title`, `description`, `priority`
3. Work through items in priority order (critical → high → medium → low)
4. Use `<promise>COMPLETION_PHRASE</promise>` when the entire task is complete
### Research Insights
Check `/home/arkon/claudeman-cases/claudeman/ralph-wizard/research/result.json` for:
- External GitHub repos and documentation links to reference
- Existing codebase patterns to follow
- Technical recommendations from the research phase
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1409",
"version": "0.1410",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+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