mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
chore: bump version to 0.1410
This commit is contained in:
@@ -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
@@ -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
@@ -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()">×</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()">×</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) {
|
||||
|
||||
@@ -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()">×</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()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard Progress Steps -->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user