From 3ab03cfd19eaf44fa6c5c097f0296b0e5c95f6cb Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 27 Jan 2026 01:02:38 +0100 Subject: [PATCH] feat: add @fix_plan.md integration (Phase 2.3) API Endpoints: - GET /api/sessions/:id/fix-plan - Generate fix plan markdown from todos - POST /api/sessions/:id/fix-plan/import - Import todos from markdown - POST /api/sessions/:id/fix-plan/write - Write @fix_plan.md to working dir - POST /api/sessions/:id/fix-plan/read - Read and import from working dir RalphTracker Methods: - generateFixPlanMarkdown() - Creates structured markdown with priority sections - importFixPlanMarkdown() - Parses markdown and imports todos with priorities UI Features: - Menu dropdown in Ralph panel (hamburger icon) - View Fix Plan modal with copy/write buttons - Import from file button - Reset Circuit Breaker button - Fix plan modal with syntax-highlighted textarea Bump version to 0.1392 Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 2 +- package.json | 2 +- src/ralph-tracker.ts | 178 +++++++++++++++ src/web/public/app.js | 215 ++++++++++++++++- src/web/public/index.html | 297 ++++++++++++++---------- src/web/public/styles.css | 470 ++++++++++++++++++++++++++++++++++++++ src/web/server.ts | 336 ++++++++++++++++++++++++++- 7 files changed, 1371 insertions(+), 129 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f6f7388a..989631fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?" -**Version**: 0.1391 (must match `package.json`) +**Version**: 0.1392 (must match `package.json`) **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty diff --git a/package.json b/package.json index a921ce88..bc587ce3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1391", + "version": "0.1392", "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", diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts index e8d90266..e99a2af9 100644 --- a/src/ralph-tracker.ts +++ b/src/ralph-tracker.ts @@ -2039,4 +2039,182 @@ export class RalphTracker extends EventEmitter { } } } + + // ========== @fix_plan.md Generation & Import ========== + + /** + * Generate @fix_plan.md content from current todos. + * Groups todos by priority and status. + * + * @returns Markdown content for @fix_plan.md + */ + generateFixPlanMarkdown(): string { + const todos = this.todos; + const lines: string[] = ['# Fix Plan', '']; + + // Group by priority + const p0: RalphTodoItem[] = []; + const p1: RalphTodoItem[] = []; + const p2: RalphTodoItem[] = []; + const noPriority: RalphTodoItem[] = []; + const completed: RalphTodoItem[] = []; + + for (const todo of todos) { + if (todo.status === 'completed') { + completed.push(todo); + } else if (todo.priority === 'P0') { + p0.push(todo); + } else if (todo.priority === 'P1') { + p1.push(todo); + } else if (todo.priority === 'P2') { + p2.push(todo); + } else { + noPriority.push(todo); + } + } + + // High Priority (P0) + if (p0.length > 0) { + lines.push('## High Priority (P0)'); + for (const todo of p0) { + const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]'; + lines.push(`- ${checkbox} ${todo.content}`); + } + lines.push(''); + } + + // Standard (P1) + if (p1.length > 0) { + lines.push('## Standard (P1)'); + for (const todo of p1) { + const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]'; + lines.push(`- ${checkbox} ${todo.content}`); + } + lines.push(''); + } + + // Nice to Have (P2) + if (p2.length > 0) { + lines.push('## Nice to Have (P2)'); + for (const todo of p2) { + const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]'; + lines.push(`- ${checkbox} ${todo.content}`); + } + lines.push(''); + } + + // Tasks (no priority) + if (noPriority.length > 0) { + lines.push('## Tasks'); + for (const todo of noPriority) { + const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]'; + lines.push(`- ${checkbox} ${todo.content}`); + } + lines.push(''); + } + + // Completed + if (completed.length > 0) { + lines.push('## Completed'); + for (const todo of completed) { + lines.push(`- [x] ${todo.content}`); + } + lines.push(''); + } + + return lines.join('\n'); + } + + /** + * Parse @fix_plan.md content and import todos. + * Replaces current todos with imported ones. + * + * @param content - Markdown content from @fix_plan.md + * @returns Number of todos imported + */ + importFixPlanMarkdown(content: string): number { + const lines = content.split('\n'); + const newTodos: RalphTodoItem[] = []; + let currentPriority: RalphTodoPriority = null; + + // Patterns for section headers + const p0HeaderPattern = /^##\s*(High Priority|Critical|P0)/i; + const p1HeaderPattern = /^##\s*(Standard|P1|Medium Priority)/i; + const p2HeaderPattern = /^##\s*(Nice to Have|P2|Low Priority)/i; + const completedHeaderPattern = /^##\s*Completed/i; + const tasksHeaderPattern = /^##\s*Tasks/i; + + // Pattern for todo items + const todoPattern = /^-\s*\[([ x\-])\]\s*(.+)$/; + + let inCompletedSection = false; + + for (const line of lines) { + const trimmed = line.trim(); + + // Check for section headers + if (p0HeaderPattern.test(trimmed)) { + currentPriority = 'P0'; + inCompletedSection = false; + continue; + } + if (p1HeaderPattern.test(trimmed)) { + currentPriority = 'P1'; + inCompletedSection = false; + continue; + } + if (p2HeaderPattern.test(trimmed)) { + currentPriority = 'P2'; + inCompletedSection = false; + continue; + } + if (completedHeaderPattern.test(trimmed)) { + inCompletedSection = true; + continue; + } + if (tasksHeaderPattern.test(trimmed)) { + currentPriority = null; + inCompletedSection = false; + continue; + } + + // Parse todo item + const match = trimmed.match(todoPattern); + if (match) { + const [, checkboxState, content] = match; + let status: RalphTodoStatus; + + if (inCompletedSection || checkboxState === 'x' || checkboxState === 'X') { + status = 'completed'; + } else if (checkboxState === '-') { + status = 'in_progress'; + } else { + status = 'pending'; + } + + // Parse priority from content if not in a priority section + const parsedPriority = inCompletedSection ? null : (currentPriority || this.parsePriority(content)); + + const id = this.generateTodoId(content); + newTodos.push({ + id, + content: content.trim(), + status, + detectedAt: Date.now(), + priority: parsedPriority, + }); + } + } + + // Replace current todos with imported ones + this._todos.clear(); + for (const todo of newTodos) { + this._todos.set(todo.id, todo); + } + + // Emit update + this.emit('todoUpdate', this.todos); + + return newTodos.length; + } } diff --git a/src/web/public/app.js b/src/web/public/app.js index 4b9e19d9..e14e416f 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -4243,10 +4243,11 @@ class ClaudemanApp { document.getElementById('appSettingsShowSystemStats').checked = settings.showSystemStats ?? true; document.getElementById('appSettingsShowTokenCount').checked = settings.showTokenCount ?? true; document.getElementById('appSettingsShowMonitor').checked = settings.showMonitor ?? true; - document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? true; - document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? true; - document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? false; document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? true; + document.getElementById('appSettingsShowFileBrowser').checked = settings.showFileBrowser ?? false; + document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? true; + document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? true; + document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? false; // Claude CLI settings const claudeModeSelect = document.getElementById('appSettingsClaudeMode'); const allowedToolsRow = document.getElementById('allowedToolsRow'); @@ -4266,10 +4267,14 @@ class ClaudemanApp { document.getElementById('appSettingsNotifCritical').checked = !notifPrefs.muteCritical; document.getElementById('appSettingsNotifWarning').checked = !notifPrefs.muteWarning; document.getElementById('appSettingsNotifInfo').checked = !notifPrefs.muteInfo; - // Update permission status display + // Update permission status display (compact format for new grid layout) const permStatus = document.getElementById('notifPermissionStatus'); if (permStatus && typeof Notification !== 'undefined') { - permStatus.textContent = `Status: ${Notification.permission}`; + const perm = Notification.permission; + permStatus.textContent = perm === 'granted' ? '\u2713' : perm === 'denied' ? '\u2717' : '?'; + permStatus.classList.remove('granted', 'denied'); + if (perm === 'granted') permStatus.classList.add('granted'); + else if (perm === 'denied') permStatus.classList.add('denied'); } // Reset to first tab and wire up tab switching this.switchSettingsTab('settings-display'); @@ -4306,10 +4311,11 @@ class ClaudemanApp { showSystemStats: document.getElementById('appSettingsShowSystemStats').checked, showTokenCount: document.getElementById('appSettingsShowTokenCount').checked, showMonitor: document.getElementById('appSettingsShowMonitor').checked, - subagentTrackingEnabled: document.getElementById('appSettingsSubagentTracking').checked, - showSubagents: document.getElementById('appSettingsShowSubagents').checked, - subagentActiveTabOnly: document.getElementById('appSettingsSubagentActiveTabOnly').checked, showProjectInsights: document.getElementById('appSettingsShowProjectInsights').checked, + showFileBrowser: document.getElementById('appSettingsShowFileBrowser').checked, + showSubagents: document.getElementById('appSettingsShowSubagents').checked, + subagentTrackingEnabled: document.getElementById('appSettingsSubagentTracking').checked, + subagentActiveTabOnly: document.getElementById('appSettingsSubagentActiveTabOnly').checked, // Claude CLI settings claudeMode: document.getElementById('appSettingsClaudeMode').value, allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(), @@ -4411,6 +4417,7 @@ class ClaudemanApp { const settings = this.loadAppSettingsFromStorage(); const showMonitor = settings.showMonitor ?? true; const showSubagents = settings.showSubagents ?? true; + const showFileBrowser = settings.showFileBrowser ?? false; const monitorPanel = document.getElementById('monitorPanel'); if (monitorPanel) { @@ -4425,6 +4432,17 @@ class ClaudemanApp { subagentsPanel.classList.add('hidden'); } } + + // File browser panel visibility + const fileBrowserPanel = document.getElementById('fileBrowserPanel'); + if (fileBrowserPanel) { + if (showFileBrowser && this.activeSessionId) { + fileBrowserPanel.classList.add('visible'); + this.loadFileBrowser(this.activeSessionId); + } else { + fileBrowserPanel.classList.remove('visible'); + } + } } closeMonitor() { @@ -5140,6 +5158,187 @@ class ClaudemanApp { this.renderRalphStatePanel(); } + // ========== @fix_plan.md Integration ========== + + toggleRalphMenu() { + const dropdown = document.getElementById('ralphDropdown'); + if (dropdown) { + dropdown.classList.toggle('show'); + } + } + + closeRalphMenu() { + const dropdown = document.getElementById('ralphDropdown'); + if (dropdown) { + dropdown.classList.remove('show'); + } + } + + async resetCircuitBreaker() { + if (!this.activeSessionId) return; + + try { + const response = await fetch(`/api/sessions/${this.activeSessionId}/ralph-circuit-breaker/reset`, { + method: 'POST', + }); + const data = await response.json(); + + if (data.success) { + this.notificationManager?.notify({ + urgency: 'info', + category: 'circuit-breaker', + title: 'Reset', + message: 'Circuit breaker reset to CLOSED', + }); + } + } catch (error) { + console.error('Error resetting circuit breaker:', error); + } + } + + /** + * Generate @fix_plan.md content and show in a modal. + */ + async showFixPlan() { + if (!this.activeSessionId) return; + + try { + const response = await fetch(`/api/sessions/${this.activeSessionId}/fix-plan`); + const data = await response.json(); + + if (!data.success) { + this.notificationManager?.notify({ + urgency: 'error', + category: 'fix-plan', + title: 'Error', + message: data.error || 'Failed to generate fix plan', + }); + return; + } + + // Show in a modal + this.showFixPlanModal(data.data.content, data.data.todoCount); + } catch (error) { + console.error('Error fetching fix plan:', error); + } + } + + /** + * Show fix plan content in a modal. + */ + showFixPlanModal(content, todoCount) { + // Create modal if it doesn't exist + let modal = document.getElementById('fixPlanModal'); + if (!modal) { + modal = document.createElement('div'); + modal.id = 'fixPlanModal'; + modal.className = 'modal'; + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + } + + document.getElementById('fixPlanContent').value = content; + document.getElementById('fixPlanStats').textContent = `${todoCount} tasks`; + modal.classList.add('show'); + } + + closeFixPlanModal() { + const modal = document.getElementById('fixPlanModal'); + if (modal) { + modal.classList.remove('show'); + } + } + + async copyFixPlan() { + const content = document.getElementById('fixPlanContent')?.value; + if (content) { + await navigator.clipboard.writeText(content); + this.notificationManager?.notify({ + urgency: 'info', + category: 'fix-plan', + title: 'Copied', + message: 'Fix plan copied to clipboard', + }); + } + } + + async writeFixPlanToFile() { + if (!this.activeSessionId) return; + + try { + const response = await fetch(`/api/sessions/${this.activeSessionId}/fix-plan/write`, { + method: 'POST', + }); + const data = await response.json(); + + if (data.success) { + this.notificationManager?.notify({ + urgency: 'info', + category: 'fix-plan', + title: 'Written', + message: `@fix_plan.md written to ${data.data.filePath}`, + }); + this.closeFixPlanModal(); + } else { + this.notificationManager?.notify({ + urgency: 'error', + category: 'fix-plan', + title: 'Error', + message: data.error || 'Failed to write file', + }); + } + } catch (error) { + console.error('Error writing fix plan:', error); + } + } + + async importFixPlanFromFile() { + if (!this.activeSessionId) return; + + try { + const response = await fetch(`/api/sessions/${this.activeSessionId}/fix-plan/read`, { + method: 'POST', + }); + const data = await response.json(); + + if (data.success) { + this.notificationManager?.notify({ + urgency: 'info', + category: 'fix-plan', + title: 'Imported', + message: `Imported ${data.data.importedCount} tasks from @fix_plan.md`, + }); + // Refresh ralph panel + this.updateRalphState(this.activeSessionId, { todos: data.data.todos }); + } else { + this.notificationManager?.notify({ + urgency: 'warning', + category: 'fix-plan', + title: 'Not Found', + message: data.error || '@fix_plan.md not found', + }); + } + } catch (error) { + console.error('Error importing fix plan:', error); + } + } + toggleRalphDetach() { const panel = this.$('ralphStatePanel'); const detachBtn = this.$('ralphDetachBtn'); diff --git a/src/web/public/index.html b/src/web/public/index.html index 91f445be..136690bb 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -122,6 +122,16 @@ ▼
+
+ +
+ + + +
+ +
+
@@ -185,6 +195,40 @@
+ +
+
+ Files +
+ + + +
+
+ +
+
Select a session to view files
+
+
+
+ + +
+
+
+ file.ts +
+ + +
+
+
+ +
+
+