diff --git a/src/inner-loop-tracker.ts b/src/inner-loop-tracker.ts index 0092f34d..2739c280 100644 --- a/src/inner-loop-tracker.ts +++ b/src/inner-loop-tracker.ts @@ -29,6 +29,19 @@ const LOOP_START_PATTERN = /Loop started at|Starting.*loop|Ralph loop started|
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
*/
diff --git a/src/types.ts b/src/types.ts
index 33b48f1a..fd044442 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -255,6 +255,7 @@ export interface InnerLoopState {
completionPhrase: string | null;
startedAt: number | null;
cycleCount: number;
+ maxIterations: number | null;
lastActivity: number;
elapsedHours: number | null;
}
@@ -283,6 +284,7 @@ export function createInitialInnerLoopState(): InnerLoopState {
completionPhrase: null,
startedAt: null,
cycleCount: 0,
+ maxIterations: null,
lastActivity: Date.now(),
elapsedHours: null,
};
diff --git a/src/web/public/app.js b/src/web/public/app.js
index c12379a7..592bab19 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -505,6 +505,10 @@ class ClaudemanApp {
existing.loop.active = false;
this.updateInnerState(data.sessionId, existing);
}
+ // Show celebration animation if this is the active session
+ if (data.sessionId === this.activeSessionId) {
+ this.showRalphCelebration(data.phrase);
+ }
});
}
@@ -1791,7 +1795,7 @@ class ClaudemanApp {
return result;
}
- // ========== Inner State (Ralph Loop & Todo List) ==========
+ // ========== Enhanced Ralph Wiggum Loop Panel ==========
updateInnerState(sessionId, updates) {
const existing = this.innerStates.get(sessionId) || { loop: null, todos: [] };
@@ -1811,10 +1815,9 @@ class ClaudemanApp {
renderInnerStatePanel() {
const panel = this.$('innerStatePanel');
- const content = this.$('innerStateContent');
- const toggle = this.$('innerStateToggle');
+ const toggle = this.$('ralphToggle');
- if (!panel) return; // Panel not in DOM yet
+ if (!panel) return;
const state = this.innerStates.get(this.activeSessionId);
@@ -1829,108 +1832,229 @@ class ClaudemanApp {
panel.style.display = '';
+ // Calculate completion percentage
+ const todos = state?.todos || [];
+ const completed = todos.filter(t => t.status === 'completed').length;
+ const total = todos.length;
+ const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
+
+ // Update progress rings
+ this.updateRalphRing(percent);
+
+ // Update status badge
+ this.updateRalphStatus(state?.loop);
+
+ // Update stats
+ this.updateRalphStats(state?.loop, completed, total);
+
+ // Update phrase preview
+ const phrasePreview = this.$('ralphPhrasePreview');
+ if (phrasePreview && state?.loop?.completionPhrase) {
+ phrasePreview.innerHTML = `${this.escapeHtml(state.loop.completionPhrase)}`;
+ } else if (phrasePreview) {
+ phrasePreview.textContent = '';
+ }
+
+ // Handle collapsed/expanded state
if (this.innerStatePanelCollapsed) {
panel.classList.add('collapsed');
- toggle.innerHTML = '▶'; // Right arrow
- content.innerHTML = this.renderInnerStateSummary(state);
+ if (toggle) toggle.innerHTML = '▶';
} else {
panel.classList.remove('collapsed');
- toggle.innerHTML = '▼'; // Down arrow
- content.innerHTML = this.renderInnerStateExpanded(state);
+ if (toggle) toggle.innerHTML = '▼';
+
+ // Update expanded view content
+ this.updateRalphExpandedView(state);
}
}
- renderInnerStateSummary(state) {
- const parts = [];
-
- // Loop status
- if (state?.loop) {
- const loop = state.loop;
- if (loop.active) {
- let loopText = '🔄 Loop';
- if (loop.completionPhrase) {
- loopText += `: "${this.escapeHtml(loop.completionPhrase)}"`;
- }
- if (loop.elapsedHours !== null) {
- loopText += ` (${loop.elapsedHours.toFixed(1)}h)`;
- }
- parts.push(`${loopText}`);
- } else if (loop.completionPhrase) {
- parts.push(`✓ ${this.escapeHtml(loop.completionPhrase)}`);
- }
+ updateRalphRing(percent) {
+ // Mini ring (in summary)
+ const miniProgress = this.$('ralphRingMiniProgress');
+ const miniText = this.$('ralphRingMiniText');
+ if (miniProgress) {
+ // Circumference = 2 * PI * r = 2 * PI * 15.9 ≈ 100
+ const offset = 100 - percent;
+ miniProgress.style.strokeDashoffset = offset;
+ }
+ if (miniText) {
+ miniText.textContent = `${percent}%`;
}
- // Todo summary
- if (state?.todos?.length > 0) {
- const completed = state.todos.filter(t => t.status === 'completed').length;
- const total = state.todos.length;
- const inProgress = state.todos.filter(t => t.status === 'in_progress').length;
- let todoText = `Tasks: ${completed}/${total}`;
- if (inProgress > 0) {
- todoText += ` (${inProgress} in progress)`;
- }
- parts.push(`${todoText}`);
+ // Large ring (in expanded view)
+ const largeProgress = this.$('ralphRingProgress');
+ const largePercent = this.$('ralphRingPercent');
+ if (largeProgress) {
+ // Circumference = 2 * PI * r = 2 * PI * 42 ≈ 264
+ const offset = 264 - (264 * percent / 100);
+ largeProgress.style.strokeDashoffset = offset;
+ }
+ if (largePercent) {
+ largePercent.textContent = `${percent}%`;
}
-
- return parts.join(' | ');
}
- renderInnerStateExpanded(state) {
- let html = '';
+ updateRalphStatus(loop) {
+ const badge = this.$('ralphStatusBadge');
+ const statusText = badge?.querySelector('.ralph-status-text');
+ if (!badge || !statusText) return;
- // Loop details
- if (state?.loop) {
- const loop = state.loop;
- html += '
${this.escapeHtml(loop.completionPhrase)}`;
- }
- html += '${this.escapeHtml(loop.completionPhrase)}