mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
fix: detect Claude Code native TodoWrite checkbox format
- Add TODO_NATIVE_PATTERN to match ☐/☒/◐ icons from Claude Code's TodoWrite - Add TODO_EXCLUDE_PATTERNS to filter out tool invocations (Bash, Search, etc.) - Update iconToStatus() to recognize ☒ as completed - Add tests for native checkbox detection The tracker now correctly parses Claude Code's actual terminal output format instead of only matching markdown checkboxes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+69
-23
@@ -23,6 +23,17 @@ const TODO_CHECKBOX_PATTERN = /^[-*]\s*\[([xX ])\]\s+(.+)$/gm;
|
||||
const TODO_INDICATOR_PATTERN = /Todo:\s*(☐|◐|✓|⏳|✅|⌛|🔄)\s+(.+)/g;
|
||||
// Format 3: Status in parentheses: "(pending)", "(in_progress)", "(completed)"
|
||||
const TODO_STATUS_PATTERN = /[-*]\s*(.+?)\s+\((pending|in_progress|completed)\)/g;
|
||||
// Format 4: Claude Code native TodoWrite output: "☐ Task", "☒ Task", "◐ Task"
|
||||
// These appear in terminal with optional leading whitespace/brackets like "⎿ ☐ Task"
|
||||
// Matches: start of line with optional whitespace/bracket, then checkbox, then task text
|
||||
const TODO_NATIVE_PATTERN = /^[\s⎿]*(☐|☒|◐)\s+([^☐☒◐\n]{3,})/gm;
|
||||
|
||||
// Patterns to exclude from todo detection (tool invocations, etc.)
|
||||
const TODO_EXCLUDE_PATTERNS = [
|
||||
/^(?:Bash|Search|Read|Write|Glob|Grep|Edit|Task)\s*\(/i, // Tool invocations
|
||||
/^(?:I'll |Let me |Now I|First,|Task \d+:|Result:|Error:)/i, // Claude commentary (with context)
|
||||
/^\S+\([^)]+\)$/, // Generic function call pattern
|
||||
];
|
||||
|
||||
// Loop status patterns
|
||||
const LOOP_START_PATTERN = /Loop started at|Starting.*loop|Ralph loop started|<promise>([^<]+)<\/promise>/i;
|
||||
@@ -33,7 +44,8 @@ const CYCLE_PATTERN = /cycle\s*#?(\d+)|respawn cycle #(\d+)/i;
|
||||
// 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"
|
||||
// Ralph loop start: "/ralph-loop:ralph-loop" command or "Starting Ralph loop"
|
||||
// Pattern matches /ralph-loop anywhere to catch both skill invocations and output
|
||||
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"
|
||||
@@ -59,12 +71,14 @@ export interface InnerLoopTrackerEvents {
|
||||
* 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).
|
||||
* patterns are detected (e.g., /ralph-loop:ralph-loop, <promise>, todos).
|
||||
*/
|
||||
export class InnerLoopTracker extends EventEmitter {
|
||||
private _loopState: InnerLoopState;
|
||||
private _todos: Map<string, InnerTodoItem> = new Map();
|
||||
private _lineBuffer: string = '';
|
||||
// Track occurrences of completion phrases to distinguish prompt from actual completion
|
||||
private _completionPhraseCount: Map<string, number> = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -148,7 +162,7 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
* Check if the data contains patterns that should auto-enable the tracker
|
||||
*/
|
||||
private shouldAutoEnable(data: string): boolean {
|
||||
// Ralph loop command: /ralph-loop
|
||||
// Ralph loop command: /ralph-loop:ralph-loop
|
||||
if (RALPH_START_PATTERN.test(data)) {
|
||||
return true;
|
||||
}
|
||||
@@ -181,6 +195,12 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Claude Code native todo format: "☐ Task", "☒ Task"
|
||||
if (TODO_NATIVE_PATTERN.test(data)) {
|
||||
TODO_NATIVE_PATTERN.lastIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop start patterns
|
||||
if (LOOP_START_PATTERN.test(data) && !PROMISE_PATTERN.test(data)) {
|
||||
return true;
|
||||
@@ -229,34 +249,37 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Handle a detected completion phrase
|
||||
*
|
||||
* Uses occurrence-based detection to distinguish prompt from actual completion:
|
||||
* - 1st occurrence: Store as expected phrase (likely in prompt)
|
||||
* - 2nd occurrence: Emit completionDetected (actual completion)
|
||||
* - If loop already active: Emit immediately (explicit loop start)
|
||||
*/
|
||||
private handleCompletionPhrase(phrase: string): void {
|
||||
// Only mark as completed if the loop was actually active
|
||||
// This prevents false positives when the completion phrase appears in the prompt itself
|
||||
if (!this._loopState.active) {
|
||||
// Just record the expected completion phrase without marking as complete
|
||||
if (!this._loopState.completionPhrase) {
|
||||
this._loopState.completionPhrase = phrase;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
return;
|
||||
const count = (this._completionPhraseCount.get(phrase) || 0) + 1;
|
||||
this._completionPhraseCount.set(phrase, count);
|
||||
|
||||
// Store phrase on first occurrence
|
||||
if (!this._loopState.completionPhrase) {
|
||||
this._loopState.completionPhrase = phrase;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
|
||||
// Loop was active, this is a real completion
|
||||
this._loopState.completionPhrase = phrase;
|
||||
this._loopState.active = false;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
|
||||
this.emit('completionDetected', phrase);
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
// Emit completion if loop is active OR this is 2nd+ occurrence
|
||||
if (this._loopState.active || count >= 2) {
|
||||
this._loopState.active = false;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('completionDetected', phrase);
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect loop start and status indicators
|
||||
*/
|
||||
private detectLoopStatus(line: string): void {
|
||||
// Check for Ralph loop start command (/ralph-loop)
|
||||
// Check for Ralph loop start command (/ralph-loop:ralph-loop)
|
||||
if (RALPH_START_PATTERN.test(line)) {
|
||||
if (!this._loopState.active) {
|
||||
this._loopState.active = true;
|
||||
@@ -382,6 +405,24 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Format 4: Claude Code native TodoWrite output (☐, ☒, ◐)
|
||||
TODO_NATIVE_PATTERN.lastIndex = 0;
|
||||
while ((match = TODO_NATIVE_PATTERN.exec(line)) !== null) {
|
||||
const icon = match[1];
|
||||
const content = match[2].trim();
|
||||
|
||||
// Skip if content matches exclude patterns (tool invocations, commentary)
|
||||
const shouldExclude = TODO_EXCLUDE_PATTERNS.some(pattern => pattern.test(content));
|
||||
if (shouldExclude) continue;
|
||||
|
||||
// Skip if content is too short or looks like partial garbage
|
||||
if (content.length < 5) continue;
|
||||
|
||||
const status = this.iconToStatus(icon);
|
||||
this.upsertTodo(content, status);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
@@ -394,13 +435,17 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
switch (icon) {
|
||||
case '✓':
|
||||
case '✅':
|
||||
case '☒': // Claude Code checked checkbox
|
||||
case '◉': // Filled circle (completed)
|
||||
case '●': // Solid circle (completed)
|
||||
return 'completed';
|
||||
case '◐':
|
||||
case '◐': // Half-filled circle (in progress)
|
||||
case '⏳':
|
||||
case '⌛':
|
||||
case '🔄':
|
||||
return 'in_progress';
|
||||
case '☐':
|
||||
case '☐': // Claude Code empty checkbox
|
||||
case '○': // Empty circle
|
||||
default:
|
||||
return 'pending';
|
||||
}
|
||||
@@ -533,6 +578,7 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this._loopState = createInitialInnerLoopState(); // This sets enabled: false
|
||||
this._todos.clear();
|
||||
this._lineBuffer = '';
|
||||
this._completionPhraseCount.clear();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
|
||||
@@ -48,12 +48,12 @@ describe('InnerLoopTracker', () => {
|
||||
expect(freshTracker.loopState.elapsedHours).toBeNull();
|
||||
});
|
||||
|
||||
it('should auto-enable on /ralph-loop command', () => {
|
||||
it('should auto-enable on /ralph-loop:ralph-loop command', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const enableHandler = vi.fn();
|
||||
freshTracker.on('enabled', enableHandler);
|
||||
|
||||
freshTracker.processTerminalData('/ralph-loop\n');
|
||||
freshTracker.processTerminalData('/ralph-loop:ralph-loop\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
expect(enableHandler).toHaveBeenCalled();
|
||||
@@ -107,7 +107,7 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
it('should reset to disabled on clear', () => {
|
||||
tracker.processTerminalData('/ralph-loop\n');
|
||||
tracker.processTerminalData('/ralph-loop:ralph-loop\n');
|
||||
expect(tracker.enabled).toBe(true);
|
||||
|
||||
tracker.clear();
|
||||
@@ -120,6 +120,8 @@ describe('InnerLoopTracker', () => {
|
||||
const completionHandler = vi.fn();
|
||||
tracker.on('completionDetected', completionHandler);
|
||||
|
||||
// Start loop first (realistic workflow)
|
||||
tracker.startLoop();
|
||||
tracker.processTerminalData('<promise>COMPLETE</promise>\n');
|
||||
|
||||
expect(completionHandler).toHaveBeenCalledWith('COMPLETE');
|
||||
@@ -130,6 +132,8 @@ describe('InnerLoopTracker', () => {
|
||||
const completionHandler = vi.fn();
|
||||
tracker.on('completionDetected', completionHandler);
|
||||
|
||||
// Start loop first (realistic workflow)
|
||||
tracker.startLoop();
|
||||
tracker.processTerminalData('Output: <promise>TIME_COMPLETE</promise>\n');
|
||||
|
||||
expect(completionHandler).toHaveBeenCalledWith('TIME_COMPLETE');
|
||||
@@ -139,6 +143,8 @@ describe('InnerLoopTracker', () => {
|
||||
const completionHandler = vi.fn();
|
||||
tracker.on('completionDetected', completionHandler);
|
||||
|
||||
// Start loop first (realistic workflow)
|
||||
tracker.startLoop();
|
||||
tracker.processTerminalData('<promise>MY_CUSTOM_PHRASE_123</promise>\n');
|
||||
|
||||
expect(completionHandler).toHaveBeenCalledWith('MY_CUSTOM_PHRASE_123');
|
||||
@@ -296,6 +302,67 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Todo Detection - Claude Code Native Format', () => {
|
||||
it('should detect pending native checkbox (☐)', () => {
|
||||
tracker.processTerminalData('☐ List files in current directory\n');
|
||||
|
||||
const todos = tracker.todos;
|
||||
expect(todos).toHaveLength(1);
|
||||
expect(todos[0].content).toBe('List files in current directory');
|
||||
expect(todos[0].status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should detect completed native checkbox (☒)', () => {
|
||||
tracker.processTerminalData('☒ Completed task\n');
|
||||
|
||||
const todos = tracker.todos;
|
||||
expect(todos).toHaveLength(1);
|
||||
expect(todos[0].status).toBe('completed');
|
||||
});
|
||||
|
||||
it('should detect todos with leading bracket (⎿)', () => {
|
||||
tracker.processTerminalData('⎿ ☐ Task with bracket\n');
|
||||
|
||||
const todos = tracker.todos;
|
||||
expect(todos).toHaveLength(1);
|
||||
expect(todos[0].content).toBe('Task with bracket');
|
||||
expect(todos[0].status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should detect todos with leading whitespace', () => {
|
||||
tracker.processTerminalData(' ☐ Indented task\n');
|
||||
|
||||
const todos = tracker.todos;
|
||||
expect(todos).toHaveLength(1);
|
||||
expect(todos[0].content).toBe('Indented task');
|
||||
});
|
||||
|
||||
it('should detect in-progress native (◐)', () => {
|
||||
tracker.processTerminalData('◐ Working on this\n');
|
||||
|
||||
const todos = tracker.todos;
|
||||
expect(todos[0].status).toBe('in_progress');
|
||||
});
|
||||
|
||||
it('should handle multiple native todos in sequence', () => {
|
||||
tracker.processTerminalData('⎿ ☐ First task\n');
|
||||
tracker.processTerminalData(' ☐ Second task\n');
|
||||
tracker.processTerminalData(' ☒ Third task\n');
|
||||
|
||||
const todos = tracker.todos;
|
||||
expect(todos).toHaveLength(3);
|
||||
expect(todos.filter(t => t.status === 'pending')).toHaveLength(2);
|
||||
expect(todos.filter(t => t.status === 'completed')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should auto-enable on native todo pattern', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
freshTracker.processTerminalData('☐ New task\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Todo Updates', () => {
|
||||
it('should update existing todos by content', () => {
|
||||
// Add pending todo
|
||||
@@ -419,11 +486,11 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
describe('Enhanced Ralph Detection Patterns', () => {
|
||||
it('should detect /ralph-loop command', () => {
|
||||
it('should detect /ralph-loop:ralph-loop command', () => {
|
||||
const loopHandler = vi.fn();
|
||||
tracker.on('loopUpdate', loopHandler);
|
||||
|
||||
tracker.processTerminalData('/ralph-loop\n');
|
||||
tracker.processTerminalData('/ralph-loop:ralph-loop\n');
|
||||
|
||||
expect(loopHandler).toHaveBeenCalled();
|
||||
expect(tracker.loopState.active).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user