mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
chore: bump version to 0.1434
This commit is contained in:
+24
-8
@@ -145,8 +145,11 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
|
||||
private _workingDir: string;
|
||||
private _homeDir: string;
|
||||
|
||||
// Track normalized paths to detect equivalents (normalized -> original)
|
||||
private _normalizedPathMap: Map<string, string> = new Map();
|
||||
// Track auto-remove timers for cleanup
|
||||
private _autoRemoveTimers: Set<ReturnType<typeof setTimeout>> = new Set();
|
||||
|
||||
// Flag to prevent operations after destroy
|
||||
private _destroyed: boolean = false;
|
||||
|
||||
// Debouncing
|
||||
private _pendingUpdate: boolean = false;
|
||||
@@ -395,7 +398,6 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
|
||||
*/
|
||||
reset(): void {
|
||||
this._activeTools.clear();
|
||||
this._normalizedPathMap.clear();
|
||||
this._lineBuffer = '';
|
||||
this._lastToolId = null;
|
||||
this.emitUpdate();
|
||||
@@ -408,7 +410,7 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
|
||||
* @param data - Raw terminal data (may include ANSI codes)
|
||||
*/
|
||||
processTerminalData(data: string): void {
|
||||
if (!this._enabled) return;
|
||||
if (!this._enabled || this._destroyed) return;
|
||||
|
||||
// Append to line buffer
|
||||
this._lineBuffer += data;
|
||||
@@ -496,10 +498,13 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
|
||||
this.scheduleUpdate();
|
||||
|
||||
// Remove completed tool after a short delay to allow UI to show completion
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
this._autoRemoveTimers.delete(timer);
|
||||
if (this._destroyed) return;
|
||||
this._activeTools.delete(tool.id);
|
||||
this.scheduleUpdate();
|
||||
}, 2000);
|
||||
this._autoRemoveTimers.add(timer);
|
||||
}
|
||||
this._lastToolId = null;
|
||||
return;
|
||||
@@ -531,10 +536,13 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
|
||||
this.scheduleUpdate();
|
||||
|
||||
// Auto-remove suggestions after 30 seconds
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
this._autoRemoveTimers.delete(timer);
|
||||
if (this._destroyed) return;
|
||||
this._activeTools.delete(tool.id);
|
||||
this.scheduleUpdate();
|
||||
}, 30000);
|
||||
this._autoRemoveTimers.add(timer);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -565,10 +573,13 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
|
||||
this.scheduleUpdate();
|
||||
|
||||
// Auto-remove after 60 seconds
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
this._autoRemoveTimers.delete(timer);
|
||||
if (this._destroyed) return;
|
||||
this._activeTools.delete(tool.id);
|
||||
this.scheduleUpdate();
|
||||
}, 60000);
|
||||
this._autoRemoveTimers.add(timer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,12 +668,17 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
|
||||
* Clean up resources.
|
||||
*/
|
||||
destroy(): void {
|
||||
this._destroyed = true;
|
||||
if (this._updateTimer) {
|
||||
clearTimeout(this._updateTimer);
|
||||
this._updateTimer = null;
|
||||
}
|
||||
// Clear all auto-remove timers to prevent orphaned callbacks
|
||||
for (const timer of this._autoRemoveTimers) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this._autoRemoveTimers.clear();
|
||||
this._activeTools.clear();
|
||||
this._normalizedPathMap.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +329,10 @@ export class FileStreamManager extends EventEmitter {
|
||||
for (const streamId of this.streams.keys()) {
|
||||
this.closeStream(streamId);
|
||||
}
|
||||
|
||||
// Clear Maps to release references
|
||||
this.streams.clear();
|
||||
this.sessionStreamCounts.clear();
|
||||
}
|
||||
|
||||
// ========== Private Methods ==========
|
||||
|
||||
+22
-6
@@ -67,9 +67,12 @@ export class ImageWatcher extends EventEmitter {
|
||||
/** Map of sessionId -> working directory path */
|
||||
private sessionDirs = new Map<string, string>();
|
||||
|
||||
/** Debounce timers for rapid image creation */
|
||||
/** Debounce timers for rapid image creation (keyed by filePath) */
|
||||
private debounceTimers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
/** Track which session owns each debounce timer (for cleanup) */
|
||||
private timerToSession = new Map<string, string>();
|
||||
|
||||
/** Whether the watcher is currently running */
|
||||
private _isRunning = false;
|
||||
|
||||
@@ -117,6 +120,7 @@ export class ImageWatcher extends EventEmitter {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.debounceTimers.clear();
|
||||
this.timerToSession.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,11 +203,21 @@ export class ImageWatcher extends EventEmitter {
|
||||
}
|
||||
this.sessionDirs.delete(sessionId);
|
||||
|
||||
// Clear any pending debounce timer for this session
|
||||
const timer = this.debounceTimers.get(sessionId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.debounceTimers.delete(sessionId);
|
||||
// Clear any pending debounce timers for this session
|
||||
// Collect keys first to avoid iterator invalidation during deletion
|
||||
const toDelete: string[] = [];
|
||||
for (const [filePath, ownerId] of this.timerToSession) {
|
||||
if (ownerId === sessionId) {
|
||||
toDelete.push(filePath);
|
||||
}
|
||||
}
|
||||
for (const filePath of toDelete) {
|
||||
const timer = this.debounceTimers.get(filePath);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.debounceTimers.delete(filePath);
|
||||
}
|
||||
this.timerToSession.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,10 +250,12 @@ export class ImageWatcher extends EventEmitter {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.debounceTimers.delete(filePath);
|
||||
this.timerToSession.delete(filePath);
|
||||
this.emitImageDetected(sessionId, filePath);
|
||||
}, DEBOUNCE_DELAY_MS);
|
||||
|
||||
this.debounceTimers.set(filePath, timer);
|
||||
this.timerToSession.set(filePath, sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1882,6 +1882,8 @@ export class RalphTracker extends EventEmitter {
|
||||
clear(): void {
|
||||
// Clear debounce timers to prevent stale emissions after clear
|
||||
this.clearDebounceTimers();
|
||||
// Stop fix plan file watcher to prevent memory leak
|
||||
this.stopWatchingFixPlan();
|
||||
this._loopState = createInitialRalphTrackerState(); // This sets enabled: false
|
||||
this._todos.clear();
|
||||
this._taskNumberToContent.clear();
|
||||
@@ -2765,4 +2767,20 @@ export class RalphTracker extends EventEmitter {
|
||||
return this._checkpointIterations.includes(currentIteration) &&
|
||||
currentIteration > this._lastCheckpointIteration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all resources and release memory.
|
||||
*
|
||||
* Call this when the session is being destroyed to prevent memory leaks.
|
||||
* Stops file watchers, clears all timers, data, and removes event listeners.
|
||||
*/
|
||||
destroy(): void {
|
||||
this.clearDebounceTimers();
|
||||
this.stopWatchingFixPlan();
|
||||
this._todos.clear();
|
||||
this._taskNumberToContent.clear();
|
||||
this._completionPhraseCount.clear();
|
||||
this._planTasks.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
+15
-14
@@ -80,6 +80,14 @@ const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
||||
// We look for the tool name followed by (description)
|
||||
const TASK_TOOL_PATTERN = /\b(Explore|Task|Bash|Plan|general-purpose)\(([^)]+)\)/g;
|
||||
|
||||
// Pre-compiled patterns for hot paths (avoid regex compilation per call)
|
||||
/** Pattern to strip leading ANSI escapes and whitespace from terminal buffer */
|
||||
const LEADING_ANSI_WHITESPACE_PATTERN = /^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/;
|
||||
/** Pattern to match Ctrl+L (form feed) characters */
|
||||
const CTRL_L_PATTERN = /\x0c/g;
|
||||
/** Pattern to split by newlines (CR or LF) */
|
||||
const NEWLINE_SPLIT_PATTERN = /\r?\n/;
|
||||
|
||||
// ============================================================================
|
||||
// Claude CLI PATH Resolution
|
||||
// ============================================================================
|
||||
@@ -297,7 +305,6 @@ export class Session extends EventEmitter {
|
||||
private _currentTaskId: string | null = null;
|
||||
// Use BufferAccumulator for hot-path buffers to reduce GC pressure
|
||||
private _terminalBuffer = new BufferAccumulator(MAX_TERMINAL_BUFFER_SIZE, TERMINAL_BUFFER_TRIM_SIZE);
|
||||
private _outputBuffer: string = '';
|
||||
private _textOutput = new BufferAccumulator(MAX_TEXT_OUTPUT_SIZE, TEXT_OUTPUT_TRIM_SIZE);
|
||||
private _errorBuffer: string = '';
|
||||
private _lastActivityAt: number;
|
||||
@@ -469,10 +476,6 @@ export class Session extends EventEmitter {
|
||||
return this._terminalBuffer.value;
|
||||
}
|
||||
|
||||
get outputBuffer(): string {
|
||||
return this._outputBuffer;
|
||||
}
|
||||
|
||||
get textOutput(): string {
|
||||
return this._textOutput.value;
|
||||
}
|
||||
@@ -787,7 +790,6 @@ export class Session extends EventEmitter {
|
||||
|
||||
this._status = 'busy';
|
||||
this._terminalBuffer.clear();
|
||||
this._outputBuffer = '';
|
||||
this._textOutput.clear();
|
||||
this._errorBuffer = '';
|
||||
this._messages = [];
|
||||
@@ -852,7 +854,7 @@ export class Session extends EventEmitter {
|
||||
// Strip: cursor movement (\x1b[nA/B/C/D), positioning (\x1b[n;nH),
|
||||
// clear screen (\x1b[2J), scroll region (\x1b[n;nr), and whitespace
|
||||
this._terminalBuffer.set(
|
||||
bufferValue.replace(/^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/, '')
|
||||
bufferValue.replace(LEADING_ANSI_WHITESPACE_PATTERN, '')
|
||||
);
|
||||
// Signal client to refresh
|
||||
this.emit('clearTerminal');
|
||||
@@ -916,7 +918,7 @@ export class Session extends EventEmitter {
|
||||
// Filter out focus escape sequences and Ctrl+L (form feed)
|
||||
const data = rawData
|
||||
.replace(FOCUS_ESCAPE_FILTER, '')
|
||||
.replace(/\x0c/g, ''); // Remove Ctrl+L
|
||||
.replace(CTRL_L_PATTERN, ''); // Remove Ctrl+L
|
||||
if (!data) return; // Skip if only filtered sequences
|
||||
|
||||
// BufferAccumulator handles auto-trimming when max size exceeded
|
||||
@@ -1033,7 +1035,6 @@ export class Session extends EventEmitter {
|
||||
|
||||
this._status = 'busy';
|
||||
this._terminalBuffer.clear();
|
||||
this._outputBuffer = '';
|
||||
this._textOutput.clear();
|
||||
this._errorBuffer = '';
|
||||
this._messages = [];
|
||||
@@ -1198,7 +1199,6 @@ export class Session extends EventEmitter {
|
||||
|
||||
this._status = 'busy';
|
||||
this._terminalBuffer.clear();
|
||||
this._outputBuffer = '';
|
||||
this._textOutput.clear();
|
||||
this._errorBuffer = '';
|
||||
this._messages = [];
|
||||
@@ -1433,7 +1433,7 @@ export class Session extends EventEmitter {
|
||||
if (!data.includes('(') || !data.includes(')')) return;
|
||||
|
||||
// Split by newlines and process each line
|
||||
const lines = data.split(/\r?\n/);
|
||||
const lines = data.split(NEWLINE_SPLIT_PATTERN);
|
||||
for (const line of lines) {
|
||||
this.parseTaskDescriptionsFromLine(line);
|
||||
}
|
||||
@@ -1803,8 +1803,11 @@ export class Session extends EventEmitter {
|
||||
this._bashToolParser.off('toolsUpdate', this._bashToolHandlers.toolsUpdate);
|
||||
this._bashToolHandlers = null;
|
||||
}
|
||||
this._bashToolParser.destroy();
|
||||
|
||||
// Destroy all trackers to release memory and stop timers
|
||||
this._bashToolParser.destroy();
|
||||
this._taskTracker.destroy();
|
||||
this._ralphTracker.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1939,7 +1942,6 @@ export class Session extends EventEmitter {
|
||||
this._currentTaskId = taskId;
|
||||
this._status = 'busy';
|
||||
this._terminalBuffer.clear();
|
||||
this._outputBuffer = '';
|
||||
this._textOutput.clear();
|
||||
this._errorBuffer = '';
|
||||
this._messages = [];
|
||||
@@ -1966,7 +1968,6 @@ export class Session extends EventEmitter {
|
||||
|
||||
clearBuffers(): void {
|
||||
this._terminalBuffer.clear();
|
||||
this._outputBuffer = '';
|
||||
this._textOutput.clear();
|
||||
this._errorBuffer = '';
|
||||
this._messages = [];
|
||||
|
||||
@@ -128,6 +128,12 @@ const STALE_IDLE_MAX_AGE_MS = 4 * 60 * 60 * 1000; // Remove idle agents older th
|
||||
const STARTUP_MAX_FILE_AGE_MS = 4 * 60 * 60 * 1000; // Only load files modified in last 4 hours on startup
|
||||
const MAX_TRACKED_AGENTS = 500; // Maximum agents to track (LRU eviction when exceeded)
|
||||
|
||||
// Internal Claude Code agent patterns to filter out (not real user-initiated subagents)
|
||||
const INTERNAL_AGENT_PATTERNS = [
|
||||
/^\[?SUGGESTION MODE/i, // Claude Code's internal suggestion mode
|
||||
/^Suggest what user might/i, // Suggestion mode prompt variant
|
||||
];
|
||||
|
||||
// Display/preview length constants
|
||||
const TEXT_PREVIEW_LENGTH = 200; // Length for text previews in tool results
|
||||
const USER_TEXT_PREVIEW_LENGTH = 80; // Length for user message previews
|
||||
@@ -156,6 +162,15 @@ export class SubagentWatcher extends EventEmitter {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a description matches internal Claude Code agent patterns.
|
||||
* These are not real user-initiated subagents and should be filtered out.
|
||||
*/
|
||||
private isInternalAgent(description: string | undefined): boolean {
|
||||
if (!description) return false;
|
||||
return INTERNAL_AGENT_PATTERNS.some(pattern => pattern.test(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract short model identifier from full model name
|
||||
*/
|
||||
@@ -925,6 +940,11 @@ export class SubagentWatcher extends EventEmitter {
|
||||
description = this.extractDescriptionFromFile(filePath);
|
||||
}
|
||||
|
||||
// Skip internal Claude Code agents (e.g., suggestion mode) - not real subagents
|
||||
if (this.isInternalAgent(description)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info: SubagentInfo = {
|
||||
agentId,
|
||||
sessionId,
|
||||
@@ -980,6 +1000,11 @@ export class SubagentWatcher extends EventEmitter {
|
||||
extractedDescription = this.extractDescriptionFromFile(filePath);
|
||||
}
|
||||
if (extractedDescription) {
|
||||
// Check if this is an internal agent - if so, remove it
|
||||
if (this.isInternalAgent(extractedDescription)) {
|
||||
this.removeAgent(agentId);
|
||||
return;
|
||||
}
|
||||
existingInfo.description = extractedDescription;
|
||||
this.emit('subagent:updated', existingInfo);
|
||||
}
|
||||
@@ -1094,6 +1119,11 @@ export class SubagentWatcher extends EventEmitter {
|
||||
}
|
||||
}
|
||||
if (description) {
|
||||
// Check if this is an internal agent - if so, remove it
|
||||
if (this.isInternalAgent(description)) {
|
||||
this.removeAgent(agentId);
|
||||
return;
|
||||
}
|
||||
info.description = description;
|
||||
this.emit('subagent:updated', info);
|
||||
}
|
||||
|
||||
@@ -564,4 +564,15 @@ export class TaskTracker extends EventEmitter {
|
||||
this.taskStack = [];
|
||||
this.pendingToolUses.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources and release memory.
|
||||
*
|
||||
* Call this when the session is being destroyed to prevent memory leaks.
|
||||
* Clears all data and removes all event listeners.
|
||||
*/
|
||||
destroy(): void {
|
||||
this.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
+164
-2
@@ -3482,6 +3482,10 @@ class ClaudemanApp {
|
||||
// Stop any existing generation first
|
||||
this.stopPlanGeneration();
|
||||
|
||||
// Close old plan subagent windows and clear their state
|
||||
// This ensures clicking on prompts shows fresh data, not cached from previous runs
|
||||
this.closePlanSubagentWindows();
|
||||
|
||||
// Reset stopped flag to allow new SSE events
|
||||
this.planGenerationStopped = false;
|
||||
|
||||
@@ -4795,23 +4799,181 @@ class ClaudemanApp {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get agent info from planSubagents Map
|
||||
const agentInfo = this.planSubagents.get(agentId);
|
||||
const isRunning = agentInfo?.status === 'running';
|
||||
const taskDescription = this.ralphWizardConfig?.taskDescription || 'No task description available';
|
||||
|
||||
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);
|
||||
// File not found - show contextual message based on agent status
|
||||
if (isRunning) {
|
||||
const content = this.buildAgentRunningContent(agentType, agentInfo, taskDescription);
|
||||
this.showPlanFileWindow(agentType, 'prompt.md', content, false);
|
||||
} else {
|
||||
// Agent completed but no prompt file - might have failed
|
||||
const content = this.buildAgentNoPromptContent(agentType, taskDescription);
|
||||
this.showPlanFileWindow(agentType, 'prompt.md', content, 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);
|
||||
if (isRunning) {
|
||||
const content = this.buildAgentRunningContent(agentType, agentInfo, taskDescription);
|
||||
this.showPlanFileWindow(agentType, 'prompt.md', content, false);
|
||||
} else {
|
||||
this.showPlanFileWindow(agentType, 'prompt.md', `Error fetching prompt: ${err.message}`, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildAgentRunningContent(agentType, agentInfo, taskDescription) {
|
||||
const typeDescriptions = {
|
||||
research: {
|
||||
title: 'Research Agent',
|
||||
role: 'Gathering external resources, exploring the codebase, and identifying patterns',
|
||||
activities: [
|
||||
'Reading CLAUDE.md and project documentation',
|
||||
'Exploring the codebase structure with Glob and Grep',
|
||||
'Searching the web for relevant documentation and examples',
|
||||
'Identifying existing patterns and conventions',
|
||||
'Finding similar implementations to reference',
|
||||
],
|
||||
},
|
||||
requirements: {
|
||||
title: 'Requirements Analyst',
|
||||
role: 'Breaking down the task into concrete, actionable requirements',
|
||||
activities: [
|
||||
'Analyzing the task description for functional requirements',
|
||||
'Identifying non-functional requirements (performance, security)',
|
||||
'Detecting edge cases and error scenarios',
|
||||
'Defining acceptance criteria for each requirement',
|
||||
],
|
||||
},
|
||||
architecture: {
|
||||
title: 'Architecture Planner',
|
||||
role: 'Designing the technical approach and file structure',
|
||||
activities: [
|
||||
'Identifying files to create or modify',
|
||||
'Planning the module/component structure',
|
||||
'Defining interfaces and data flow',
|
||||
'Ensuring consistency with existing architecture',
|
||||
],
|
||||
},
|
||||
testing: {
|
||||
title: 'TDD Specialist',
|
||||
role: 'Planning the test-first development strategy',
|
||||
activities: [
|
||||
'Identifying test cases for each requirement',
|
||||
'Planning unit, integration, and E2E tests',
|
||||
'Defining test fixtures and mocks needed',
|
||||
'Ensuring edge cases have test coverage',
|
||||
],
|
||||
},
|
||||
risks: {
|
||||
title: 'Risk Analyst',
|
||||
role: 'Identifying potential issues and mitigation strategies',
|
||||
activities: [
|
||||
'Analyzing potential failure points',
|
||||
'Identifying security concerns',
|
||||
'Flagging performance bottlenecks',
|
||||
'Suggesting rollback strategies',
|
||||
],
|
||||
},
|
||||
verification: {
|
||||
title: 'Verification Expert',
|
||||
role: 'Validating plan completeness and quality',
|
||||
activities: [
|
||||
'Checking all requirements have implementation steps',
|
||||
'Verifying test coverage is adequate',
|
||||
'Ensuring dependencies are correctly ordered',
|
||||
'Scoring plan quality and completeness',
|
||||
],
|
||||
},
|
||||
'execution-optimizer': {
|
||||
title: 'Execution Optimizer',
|
||||
role: 'Optimizing the plan for Claude Code execution',
|
||||
activities: [
|
||||
'Identifying tasks that can run in parallel',
|
||||
'Grouping related tasks for efficiency',
|
||||
'Marking fresh context requirements',
|
||||
'Estimating token usage per task',
|
||||
],
|
||||
},
|
||||
'final-review': {
|
||||
title: 'Final Reviewer',
|
||||
role: 'Performing holistic validation of the complete plan',
|
||||
activities: [
|
||||
'Checking for gaps or missing steps',
|
||||
'Validating task ordering and dependencies',
|
||||
'Ensuring the plan is implementable',
|
||||
'Final quality assessment',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const info = typeDescriptions[agentType] || {
|
||||
title: agentType,
|
||||
role: 'Processing the task',
|
||||
activities: ['Working on the task...'],
|
||||
};
|
||||
|
||||
const elapsed = agentInfo?.startTime
|
||||
? Math.floor((Date.now() - agentInfo.startTime) / 1000)
|
||||
: 0;
|
||||
const model = agentInfo?.model || 'unknown';
|
||||
|
||||
let content = `# ${info.title} - Still Running\n\n`;
|
||||
content += `⏳ **Status:** Generating... (${elapsed}s elapsed)\n`;
|
||||
content += `🤖 **Model:** ${model}\n\n`;
|
||||
content += `---\n\n`;
|
||||
content += `## Role\n${info.role}\n\n`;
|
||||
content += `## Currently Working On\n`;
|
||||
info.activities.forEach(activity => {
|
||||
content += `- ${activity}\n`;
|
||||
});
|
||||
content += `\n---\n\n`;
|
||||
content += `## Task Being Analyzed\n\n`;
|
||||
content += `\`\`\`\n${taskDescription}\n\`\`\`\n\n`;
|
||||
content += `---\n\n`;
|
||||
content += `*The full prompt will be available once the agent completes.*\n`;
|
||||
content += `*Click again after completion to see the actual prompt that was used.*`;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
buildAgentNoPromptContent(agentType, taskDescription) {
|
||||
const typeLabels = {
|
||||
research: 'Research Agent',
|
||||
requirements: 'Requirements Analyst',
|
||||
architecture: 'Architecture Planner',
|
||||
testing: 'TDD Specialist',
|
||||
risks: 'Risk Analyst',
|
||||
verification: 'Verification Expert',
|
||||
'execution-optimizer': 'Execution Optimizer',
|
||||
'final-review': 'Final Reviewer',
|
||||
};
|
||||
|
||||
let content = `# ${typeLabels[agentType] || agentType} - No Prompt Available\n\n`;
|
||||
content += `The prompt file for this agent was not saved.\n\n`;
|
||||
content += `This can happen if:\n`;
|
||||
content += `- The agent failed before completing\n`;
|
||||
content += `- The plan generation was cancelled\n`;
|
||||
content += `- There was an error saving the output\n\n`;
|
||||
content += `---\n\n`;
|
||||
content += `## Task Description\n\n`;
|
||||
content += `\`\`\`\n${taskDescription}\n\`\`\``;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
async showPlanAgentFiles(agentId, agentType) {
|
||||
const caseName = this.ralphWizardConfig?.caseName;
|
||||
if (!caseName) {
|
||||
|
||||
+69
-84
@@ -426,15 +426,19 @@
|
||||
<div class="modal-tab-content" id="respawn-tab">
|
||||
<!-- Respawn Settings Section -->
|
||||
<div class="session-respawn-section" id="sessionRespawnSection">
|
||||
<div class="section-divider">
|
||||
<div class="respawn-header">
|
||||
<div class="session-respawn-status" id="sessionRespawnStatus">
|
||||
<span class="respawn-status-indicator"></span>
|
||||
<span class="respawn-status-text">Not active</span>
|
||||
</div>
|
||||
<div class="respawn-actions">
|
||||
<button class="btn-toolbar btn-success btn-sm" onclick="app.enableRespawnFromModal()" id="modalEnableRespawnBtn">Enable</button>
|
||||
<button class="btn-toolbar btn-danger btn-sm" onclick="app.stopRespawnFromModal()" id="modalStopRespawnBtn" style="display: none;">Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Run Duration</label>
|
||||
<label>Duration</label>
|
||||
<div class="duration-presets">
|
||||
<button type="button" class="duration-preset-btn" data-minutes="30" onclick="app.selectDurationPreset('30')">30m</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="60" onclick="app.selectDurationPreset('60')">1h</button>
|
||||
@@ -450,92 +454,81 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-hint">How long to run the respawn loop</span>
|
||||
</div>
|
||||
|
||||
<div class="form-section-header">Prompts</div>
|
||||
<div class="form-row">
|
||||
<label>Update Prompt</label>
|
||||
<input type="text" id="modalRespawnPrompt" value="update all the docs and CLAUDE.md" placeholder="Prompt to send when idle" onchange="app.autoSaveRespawnConfig()">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>After Update Prompt</label>
|
||||
<div class="checkbox-group-inline">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnSendClear" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Send /clear</span>
|
||||
</label>
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnSendInit" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Send /init</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Kickstart Prompt</label>
|
||||
<input type="text" id="modalRespawnKickstart" value="" placeholder="Optional: prompt if /init doesn't trigger work" onchange="app.autoSaveRespawnConfig()">
|
||||
<span class="form-hint">Sent only when /init completes but Claude stays idle</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Auto-Accept Plan Mode</label>
|
||||
<div class="checkbox-group-inline">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnAutoAccept" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Accept plan mode approvals</span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="form-hint">Presses Enter to accept plan approvals and default question options when Claude is waiting</span>
|
||||
</div>
|
||||
|
||||
<div class="respawn-actions">
|
||||
<button class="btn-toolbar btn-success" onclick="app.enableRespawnFromModal()" id="modalEnableRespawnBtn">Enable Respawn</button>
|
||||
<button class="btn-toolbar btn-danger" onclick="app.stopRespawnFromModal()" id="modalStopRespawnBtn" style="display: none;">Stop Respawn</button>
|
||||
<div class="form-section-header">Behavior</div>
|
||||
<div class="respawn-options-row">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnSendClear" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Send /clear</span>
|
||||
</label>
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnSendInit" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Send /init</span>
|
||||
</label>
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnAutoAccept" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Auto-accept prompts</span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="form-hint">Auto-accept presses Enter for plan approvals and default question options</span>
|
||||
</div>
|
||||
</div><!-- End respawn-tab -->
|
||||
|
||||
<!-- Context Management Tab Content -->
|
||||
<div class="modal-tab-content hidden" id="context-tab">
|
||||
<div class="form-row">
|
||||
<label>Auto-Compact Context</label>
|
||||
<div class="auto-clear-row">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalAutoCompactEnabled" onchange="app.autoSaveAutoCompact()">
|
||||
<span>Enable at</span>
|
||||
</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalAutoCompactThreshold" value="110000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoCompact()">
|
||||
<span>tokens</span>
|
||||
<div class="form-section-header">Token Management</div>
|
||||
<div class="context-settings-grid">
|
||||
<div class="context-setting">
|
||||
<div class="context-setting-header">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalAutoCompactEnabled" onchange="app.autoSaveAutoCompact()">
|
||||
<span>Auto-Compact</span>
|
||||
</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalAutoCompactThreshold" value="110000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoCompact()">
|
||||
<span>tokens</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="modalAutoCompactPrompt" value="" placeholder="Focus prompt (optional)" onchange="app.autoSaveAutoCompact()">
|
||||
<span class="form-hint">Summarizes context while preserving key information</span>
|
||||
</div>
|
||||
<input type="text" id="modalAutoCompactPrompt" value="" placeholder="Optional: focus prompt for compact" class="mt-sm" onchange="app.autoSaveAutoCompact()">
|
||||
<span class="form-hint">Summarizes context while preserving key information</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Auto-Clear Context</label>
|
||||
<div class="auto-clear-row">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalAutoClearEnabled" onchange="app.autoSaveAutoClear()">
|
||||
<span>Enable at</span>
|
||||
</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalAutoClearThreshold" value="140000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoClear()">
|
||||
<span>tokens</span>
|
||||
<div class="context-setting">
|
||||
<div class="context-setting-header">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalAutoClearEnabled" onchange="app.autoSaveAutoClear()">
|
||||
<span>Auto-Clear</span>
|
||||
</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalAutoClearThreshold" value="140000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoClear()">
|
||||
<span>tokens</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-hint">Full context reset at threshold (use higher than compact)</span>
|
||||
</div>
|
||||
<span class="form-hint">Completely resets context (use higher threshold than compact)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-section-header">File Watching</div>
|
||||
<div class="form-row form-row-switch">
|
||||
<label>Image Watcher</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="modalImageWatcherEnabled" checked onchange="app.toggleSessionImageWatcher()">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="form-hint">Auto-popup new images (screenshots, etc.) in this session's directory</span>
|
||||
<span class="form-hint">Auto-popup new images (screenshots, generated files) in this session's directory</span>
|
||||
</div>
|
||||
</div><!-- End context-tab -->
|
||||
|
||||
@@ -547,34 +540,34 @@
|
||||
<input type="checkbox" id="modalRalphEnabled">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="form-hint">Enable Ralph / Todo tracker for this session (auto-detects loops and todos)</span>
|
||||
<span class="form-hint">Auto-detects loops and todos in Claude output</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Completion Phrase</label>
|
||||
<input type="text" id="modalRalphPhrase" placeholder="e.g., COMPLETE, DONE, FINISHED">
|
||||
<span class="form-hint">The phrase Claude outputs when the loop is complete (without <promise> tags)</span>
|
||||
<span class="form-hint">Phrase Claude outputs when loop is complete (without <promise> tags)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Max Iterations</label>
|
||||
<input type="number" id="modalRalphMaxIterations" value="0" min="0" max="1000">
|
||||
<span class="form-hint">Maximum respawn cycles (0 = unlimited)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Max Todos</label>
|
||||
<input type="number" id="modalRalphMaxTodos" value="50" min="1" max="500">
|
||||
<span class="form-hint">Maximum todos to track per session</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Todo Expiration</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalRalphTodoExpiration" value="60" min="1" max="1440">
|
||||
<span>min</span>
|
||||
<div class="form-section-header">Limits</div>
|
||||
<div class="ralph-limits-grid">
|
||||
<div class="form-col">
|
||||
<label>Max Iterations</label>
|
||||
<input type="number" id="modalRalphMaxIterations" value="0" min="0" max="1000">
|
||||
<span class="form-hint">0 = unlimited</span>
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<label>Max Todos</label>
|
||||
<input type="number" id="modalRalphMaxTodos" value="50" min="1" max="500">
|
||||
<span class="form-hint">Per session</span>
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<label>Todo Expiration</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalRalphTodoExpiration" value="60" min="1" max="1440">
|
||||
<span>min</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-hint">Minutes before todos auto-expire</span>
|
||||
</div>
|
||||
|
||||
<div class="ralph-config-actions">
|
||||
@@ -1153,14 +1146,6 @@
|
||||
<details class="advanced-options">
|
||||
<summary>Advanced Options</summary>
|
||||
<div class="advanced-options-content">
|
||||
<div class="form-row form-row-switch">
|
||||
<label>Auto-Start After Plan</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="ralphAutoStart">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="form-hint">Automatically start the loop when plan generation completes</span>
|
||||
</div>
|
||||
<div class="form-row form-row-switch">
|
||||
<label>Enable Respawn</label>
|
||||
<label class="switch">
|
||||
|
||||
+125
-4
@@ -1754,6 +1754,8 @@ body {
|
||||
|
||||
.btn-toolbar.btn-claude svg {
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Shell Button - Special styling */
|
||||
@@ -2492,6 +2494,21 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Fix CSS specificity: ID selector overrides class selector, so we need this explicit rule */
|
||||
#summary-tab.hidden,
|
||||
#respawn-tab.hidden,
|
||||
#context-tab.hidden,
|
||||
#ralph-tab.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Also hide child elements when parent tab is hidden (belt and suspenders) */
|
||||
#summary-tab.hidden .run-summary-filters,
|
||||
#summary-tab.hidden .run-summary-timeline,
|
||||
#summary-tab.hidden .run-summary-footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#summary-tab .run-summary-filters {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -2958,13 +2975,35 @@ body {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
/* Respawn header with status and actions */
|
||||
.respawn-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.respawn-header .respawn-actions {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Respawn options row - inline checkboxes */
|
||||
.respawn-options-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Respawn actions in modal */
|
||||
.respawn-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.session-respawn-actions {
|
||||
@@ -3003,7 +3042,89 @@ body {
|
||||
}
|
||||
|
||||
.modal-tab-content.hidden {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Context Tab Settings Grid */
|
||||
.context-settings-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.context-setting {
|
||||
padding: 0.875rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.context-setting-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.context-setting-header .checkbox-inline span {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.context-setting-header .input-suffix-sm {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-setting-header .input-suffix-sm input {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.context-setting input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.context-setting input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.context-setting .form-hint {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Ralph Limits Grid */
|
||||
.ralph-limits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.ralph-limits-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.ralph-limits-grid .form-col input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ralph-limits-grid .form-col .form-hint {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
/* Ralph Config Actions */
|
||||
|
||||
+19
-2
@@ -14,7 +14,7 @@ import Fastify, { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import path, { join, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { homedir, totalmem, freemem, loadavg, cpus } from 'node:os';
|
||||
@@ -2518,6 +2518,17 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
const resolvedBase = resolve(casesDir);
|
||||
if (resolvedCase.startsWith(resolvedBase) && existsSync(casePath)) {
|
||||
outputDir = join(casePath, 'ralph-wizard');
|
||||
|
||||
// Clear old ralph-wizard directory to ensure fresh prompts for each generation
|
||||
// This prevents stale prompts from previous runs being shown when clicking on agents
|
||||
if (existsSync(outputDir)) {
|
||||
try {
|
||||
rmSync(outputDir, { recursive: true, force: true });
|
||||
console.log(`[API] Cleared old ralph-wizard directory: ${outputDir}`);
|
||||
} catch (err) {
|
||||
console.warn(`[API] Failed to clear ralph-wizard directory:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2665,11 +2676,17 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
});
|
||||
|
||||
// Read a specific ralph-wizard file
|
||||
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req) => {
|
||||
// Cache disabled to ensure fresh prompts when starting new plan generations
|
||||
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req, reply) => {
|
||||
const { caseName, filePath } = req.params as { caseName: string; filePath: string };
|
||||
const casesDir = join(homedir(), 'claudeman-cases');
|
||||
const casePath = join(casesDir, caseName);
|
||||
|
||||
// Prevent browser caching - prompts change between plan generations
|
||||
reply.header('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
reply.header('Pragma', 'no-cache');
|
||||
reply.header('Expires', '0');
|
||||
|
||||
// Security: Path traversal protection for case name
|
||||
const resolvedCase = resolve(casePath);
|
||||
const resolvedBase = resolve(casesDir);
|
||||
|
||||
Reference in New Issue
Block a user