mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
feat(tracker): disable Ralph Wiggum tracker by default, auto-enable on detection
The InnerLoopTracker is now disabled by default and auto-enables when Ralph-related patterns are detected: - /ralph-loop command - <promise>PHRASE</promise> completion phrases - TodoWrite tool usage - Iteration patterns (Iteration 5/50, [5/50]) - Todo checkboxes (- [ ]/- [x]) or indicator icons This reduces noise for sessions that don't use Ralph loops while maintaining full functionality when loops are detected. Changes: - Add `enabled` property to InnerLoopState type - Add enable()/disable() methods to InnerLoopTracker - Implement shouldAutoEnable() for pattern detection - Update frontend to show "Tracking" status when enabled - Add CSS for tracking state indicator - Update CLAUDE.md documentation - Add comprehensive tests for auto-enable behavior Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+102
-2
@@ -50,12 +50,16 @@ export interface InnerLoopTrackerEvents {
|
||||
loopUpdate: (state: InnerLoopState) => void;
|
||||
todoUpdate: (todos: InnerTodoItem[]) => void;
|
||||
completionDetected: (phrase: string) => void;
|
||||
enabled: () => void; // Emitted when tracker auto-enables
|
||||
}
|
||||
|
||||
/**
|
||||
* InnerLoopTracker parses terminal output from Claude Code sessions to detect:
|
||||
* 1. Ralph Wiggum loop state (active, completion phrase, cycle count)
|
||||
* 2. Todo list items from the TodoWrite tool
|
||||
*
|
||||
* The tracker is DISABLED by default and auto-enables when Ralph-related
|
||||
* patterns are detected (e.g., /ralph-loop, <promise>, todos).
|
||||
*/
|
||||
export class InnerLoopTracker extends EventEmitter {
|
||||
private _loopState: InnerLoopState;
|
||||
@@ -67,6 +71,36 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this._loopState = createInitialInnerLoopState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the tracker is enabled and actively monitoring
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this._loopState.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the tracker (called automatically when Ralph patterns detected)
|
||||
*/
|
||||
enable(): void {
|
||||
if (!this._loopState.enabled) {
|
||||
this._loopState.enabled = true;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('enabled');
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the tracker
|
||||
*/
|
||||
disable(): void {
|
||||
if (this._loopState.enabled) {
|
||||
this._loopState.enabled = false;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
get loopState(): InnerLoopState {
|
||||
return { ...this._loopState };
|
||||
}
|
||||
@@ -82,6 +116,16 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
// Remove ANSI escape codes for cleaner parsing
|
||||
const cleanData = data.replace(ANSI_ESCAPE_PATTERN, '');
|
||||
|
||||
// If tracker is disabled, only check for patterns that should auto-enable it
|
||||
if (!this._loopState.enabled) {
|
||||
if (this.shouldAutoEnable(cleanData)) {
|
||||
this.enable();
|
||||
// Continue processing now that we're enabled
|
||||
} else {
|
||||
return; // Don't process further when disabled
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer data for line-based processing
|
||||
this._lineBuffer += cleanData;
|
||||
|
||||
@@ -100,6 +144,51 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this.cleanupExpiredTodos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the data contains patterns that should auto-enable the tracker
|
||||
*/
|
||||
private shouldAutoEnable(data: string): boolean {
|
||||
// Ralph loop command: /ralph-loop
|
||||
if (RALPH_START_PATTERN.test(data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Completion phrase: <promise>...</promise>
|
||||
if (PROMISE_PATTERN.test(data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TodoWrite tool usage
|
||||
if (TODOWRITE_PATTERN.test(data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Iteration patterns from Ralph loop: "Iteration 5/50", "[5/50]"
|
||||
if (ITERATION_PATTERN.test(data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Todo checkboxes: "- [ ] Task" or "- [x] Task"
|
||||
if (TODO_CHECKBOX_PATTERN.test(data)) {
|
||||
// Reset lastIndex since we're reusing the global regex
|
||||
TODO_CHECKBOX_PATTERN.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Todo indicator icons: "Todo: ☐", "Todo: ◐", etc.
|
||||
if (TODO_INDICATOR_PATTERN.test(data)) {
|
||||
TODO_INDICATOR_PATTERN.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop start patterns
|
||||
if (LOOP_START_PATTERN.test(data) && !PROMISE_PATTERN.test(data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single line of terminal output
|
||||
*/
|
||||
@@ -388,8 +477,14 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Mark the loop as started (can be called externally)
|
||||
* Also enables the tracker if not already enabled
|
||||
*/
|
||||
startLoop(completionPhrase?: string, maxIterations?: number): void {
|
||||
// Enable tracker when loop is explicitly started
|
||||
if (!this._loopState.enabled) {
|
||||
this._loopState.enabled = true;
|
||||
this.emit('enabled');
|
||||
}
|
||||
this._loopState.active = true;
|
||||
this._loopState.startedAt = Date.now();
|
||||
this._loopState.cycleCount = 0;
|
||||
@@ -422,9 +517,10 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Clear all state (e.g., when session is cleared)
|
||||
* Resets to disabled state
|
||||
*/
|
||||
clear(): void {
|
||||
this._loopState = createInitialInnerLoopState();
|
||||
this._loopState = createInitialInnerLoopState(); // This sets enabled: false
|
||||
this._todos.clear();
|
||||
this._lineBuffer = '';
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
@@ -465,7 +561,11 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
* Restore state from persisted data
|
||||
*/
|
||||
restoreState(loopState: InnerLoopState, todos: InnerTodoItem[]): void {
|
||||
this._loopState = { ...loopState };
|
||||
// Ensure enabled flag exists (backwards compatibility)
|
||||
this._loopState = {
|
||||
...loopState,
|
||||
enabled: loopState.enabled ?? false, // Override after spread for backwards compat
|
||||
};
|
||||
this._todos.clear();
|
||||
for (const todo of todos) {
|
||||
this._todos.set(todo.id, { ...todo });
|
||||
|
||||
@@ -251,6 +251,7 @@ export const DEFAULT_CONFIG: AppConfig = {
|
||||
export type InnerTodoStatus = 'pending' | 'in_progress' | 'completed';
|
||||
|
||||
export interface InnerLoopState {
|
||||
enabled: boolean; // Whether the tracker is actively monitoring (disabled by default)
|
||||
active: boolean;
|
||||
completionPhrase: string | null;
|
||||
startedAt: number | null;
|
||||
@@ -280,6 +281,7 @@ export interface InnerStateRecord {
|
||||
|
||||
export function createInitialInnerLoopState(): InnerLoopState {
|
||||
return {
|
||||
enabled: false, // Disabled by default, auto-enables when Ralph patterns detected
|
||||
active: false,
|
||||
completionPhrase: null,
|
||||
startedAt: null,
|
||||
|
||||
@@ -1919,10 +1919,12 @@ class ClaudemanApp {
|
||||
const state = this.innerStates.get(this.activeSessionId);
|
||||
|
||||
// Check if there's anything to show
|
||||
// Only show panel if tracker is enabled OR there's active state to display
|
||||
const isEnabled = state?.loop?.enabled === true;
|
||||
const hasLoop = state?.loop?.active || state?.loop?.completionPhrase;
|
||||
const hasTodos = state?.todos?.length > 0;
|
||||
|
||||
if (!hasLoop && !hasTodos) {
|
||||
if (!isEnabled && !hasLoop && !hasTodos) {
|
||||
panel.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
@@ -1996,7 +1998,7 @@ class ClaudemanApp {
|
||||
const statusText = badge?.querySelector('.ralph-status-text');
|
||||
if (!badge || !statusText) return;
|
||||
|
||||
badge.classList.remove('active', 'completed');
|
||||
badge.classList.remove('active', 'completed', 'tracking');
|
||||
|
||||
if (loop?.active) {
|
||||
badge.classList.add('active');
|
||||
@@ -2004,6 +2006,9 @@ class ClaudemanApp {
|
||||
} else if (loop?.completionPhrase && !loop?.active) {
|
||||
badge.classList.add('completed');
|
||||
statusText.textContent = 'Complete';
|
||||
} else if (loop?.enabled) {
|
||||
badge.classList.add('tracking');
|
||||
statusText.textContent = 'Tracking';
|
||||
} else {
|
||||
statusText.textContent = 'Idle';
|
||||
}
|
||||
|
||||
@@ -556,6 +556,10 @@ body {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.ralph-status-badge.tracking .ralph-status-dot {
|
||||
background: var(--yellow);
|
||||
}
|
||||
|
||||
@keyframes ralph-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.2); }
|
||||
|
||||
Reference in New Issue
Block a user