chore: bump version to 0.1434

This commit is contained in:
arkon
2026-01-30 11:30:51 +01:00
parent da5d86a84f
commit 7dfd97df58
15 changed files with 1300 additions and 122 deletions
+24 -8
View File
@@ -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();
}
}
+4
View File
@@ -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
View File
@@ -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);
}
/**
+18
View File
@@ -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
View File
@@ -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 = [];
+30
View File
@@ -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);
}
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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 &lt;promise&gt; tags)</span>
<span class="form-hint">Phrase Claude outputs when loop is complete (without &lt;promise&gt; 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
View File
@@ -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
View File
@@ -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);