feat(ralph): enhance Ralph Wiggum loop visualization

Add polished, animated Ralph Wiggum Loop panel with:
- Circular progress ring showing task completion percentage
- Task cards with status indicators (pending/in-progress/completed)
- Animated status badge with pulsing indicator when active
- Completion celebration animation with animated checkmark
- Smart time formatting ("1h 23m" instead of "1.38 hours")

Enhanced detection patterns based on official Ralph plugin:
- /ralph-loop command detection
- Iteration patterns: "Iteration 5/50", "[5/50]"
- Max iterations from YAML: "max-iterations: 50"
- TodoWrite tool output detection

Adds maxIterations field to InnerLoopState for tracking loop limits.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 17:09:56 +01:00
co-authored by Claude Opus 4.5
parent 1ffefa41aa
commit 684ccc2b07
6 changed files with 859 additions and 192 deletions
+80 -3
View File
@@ -29,6 +29,19 @@ const LOOP_START_PATTERN = /Loop started at|Starting.*loop|Ralph loop started|<p
const ELAPSED_TIME_PATTERN = /Elapsed:\s*(\d+(?:\.\d+)?)\s*hours?/i;
const CYCLE_PATTERN = /cycle\s*#?(\d+)|respawn cycle #(\d+)/i;
// New patterns for improved Ralph detection (based on official Ralph Wiggum plugin)
// Iteration patterns: "Iteration 5/50", "[5/50]", "iteration #5"
const ITERATION_PATTERN = /(?:iteration|iter\.?)\s*#?(\d+)(?:\s*[\/of]\s*(\d+))?|\[(\d+)\/(\d+)\]/i;
// Ralph loop start: "/ralph-loop" command or "Starting Ralph loop"
const RALPH_START_PATTERN = /\/ralph-loop|starting ralph(?:\s+wiggum)?\s+loop|ralph loop (?:started|beginning)/i;
// Max iterations: "max-iterations 50" or "maxIterations: 50" or "max_iterations=50"
const MAX_ITERATIONS_PATTERN = /max[_-]?iterations?\s*[=:]\s*(\d+)/i;
// TodoWrite tool output - detect the tool being used
const TODOWRITE_PATTERN = /TodoWrite|todo(?:s)?\s*(?:updated|written|saved)|Todos have been modified/i;
// ANSI escape code removal for cleaner parsing
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
@@ -143,7 +156,20 @@ export class InnerLoopTracker extends EventEmitter {
* Detect loop start and status indicators
*/
private detectLoopStatus(line: string): void {
// Check for loop start
// Check for Ralph loop start command (/ralph-loop)
if (RALPH_START_PATTERN.test(line)) {
if (!this._loopState.active) {
this._loopState.active = true;
this._loopState.startedAt = Date.now();
this._loopState.cycleCount = 0;
this._loopState.maxIterations = null;
this._loopState.elapsedHours = null;
this._loopState.lastActivity = Date.now();
this.emit('loopUpdate', this.loopState);
}
}
// Check for generic loop start
if (LOOP_START_PATTERN.test(line)) {
// Check if this is a promise match (loop ending) vs loop start
if (!PROMISE_PATTERN.test(line)) {
@@ -152,6 +178,7 @@ export class InnerLoopTracker extends EventEmitter {
this._loopState.active = true;
this._loopState.startedAt = Date.now();
this._loopState.cycleCount = 0;
this._loopState.maxIterations = null;
this._loopState.elapsedHours = null;
this._loopState.lastActivity = Date.now();
this.emit('loopUpdate', this.loopState);
@@ -159,6 +186,40 @@ export class InnerLoopTracker extends EventEmitter {
}
}
// Check for max iterations setting
const maxIterMatch = line.match(MAX_ITERATIONS_PATTERN);
if (maxIterMatch) {
const maxIter = parseInt(maxIterMatch[1]);
if (!isNaN(maxIter) && maxIter > 0) {
this._loopState.maxIterations = maxIter;
this._loopState.lastActivity = Date.now();
this.emit('loopUpdate', this.loopState);
}
}
// Check for iteration patterns: "Iteration 5/50", "[5/50]"
const iterMatch = line.match(ITERATION_PATTERN);
if (iterMatch) {
// Pattern captures: group 1&2 for "Iteration X/Y", group 3&4 for "[X/Y]"
const currentIter = parseInt(iterMatch[1] || iterMatch[3]);
const maxIter = iterMatch[2] || iterMatch[4] ? parseInt(iterMatch[2] || iterMatch[4]) : null;
if (!isNaN(currentIter)) {
// If not already active, start the loop
if (!this._loopState.active) {
this._loopState.active = true;
this._loopState.startedAt = Date.now();
}
this._loopState.cycleCount = currentIter;
if (maxIter !== null && !isNaN(maxIter)) {
this._loopState.maxIterations = maxIter;
}
this._loopState.lastActivity = Date.now();
this.emit('loopUpdate', this.loopState);
}
}
// Check for elapsed time
const elapsedMatch = line.match(ELAPSED_TIME_PATTERN);
if (elapsedMatch) {
@@ -167,7 +228,7 @@ export class InnerLoopTracker extends EventEmitter {
this.emit('loopUpdate', this.loopState);
}
// Check for cycle count
// Check for cycle count (legacy pattern)
const cycleMatch = line.match(CYCLE_PATTERN);
if (cycleMatch) {
const cycleNum = parseInt(cycleMatch[1] || cycleMatch[2]);
@@ -177,6 +238,12 @@ export class InnerLoopTracker extends EventEmitter {
this.emit('loopUpdate', this.loopState);
}
}
// Check for TodoWrite tool usage - indicates active task tracking
if (TODOWRITE_PATTERN.test(line)) {
this._loopState.lastActivity = Date.now();
// Don't emit update just for activity, let todo detection handle it
}
}
/**
@@ -321,10 +388,11 @@ export class InnerLoopTracker extends EventEmitter {
/**
* Mark the loop as started (can be called externally)
*/
startLoop(completionPhrase?: string): void {
startLoop(completionPhrase?: string, maxIterations?: number): void {
this._loopState.active = true;
this._loopState.startedAt = Date.now();
this._loopState.cycleCount = 0;
this._loopState.maxIterations = maxIterations ?? null;
this._loopState.elapsedHours = null;
this._loopState.lastActivity = Date.now();
if (completionPhrase) {
@@ -333,6 +401,15 @@ export class InnerLoopTracker extends EventEmitter {
this.emit('loopUpdate', this.loopState);
}
/**
* Update max iterations (can be called externally)
*/
setMaxIterations(maxIterations: number | null): void {
this._loopState.maxIterations = maxIterations;
this._loopState.lastActivity = Date.now();
this.emit('loopUpdate', this.loopState);
}
/**
* Mark the loop as stopped
*/