2026-03-01 04:04:35 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* @fileoverview Pure utility functions for terminal pattern detection in respawn controller.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Extracted from respawn-controller.ts for modularity. These are stateless functions
|
|
|
|
|
|
* and constants used to detect completion messages, working patterns, and token counts
|
|
|
|
|
|
* in terminal output.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @module respawn-patterns
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-08-09 15:31:02 +02:00
|
|
|
|
import { TOKEN_PATTERN, CLAUDE_WORKING_LINE_PATTERN } from './utils/index.js';
|
2026-03-01 04:04:35 +01:00
|
|
|
|
|
|
|
|
|
|
// ========== Constants ==========
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Pattern to detect completion messages from Claude Code.
|
|
|
|
|
|
* Requires "Worked for" prefix to avoid false positives from bare time durations
|
|
|
|
|
|
* in regular text (e.g., "wait for 5s", "run for 2m").
|
|
|
|
|
|
*
|
|
|
|
|
|
* Matches: "Worked for 2m 46s", "Worked for 46s", "Worked for 1h 2m 3s"
|
|
|
|
|
|
* Does NOT match: "wait for 5s", "run for 2m", "for 3s the system..."
|
|
|
|
|
|
*/
|
|
|
|
|
|
const COMPLETION_TIME_PATTERN = /\bWorked\s+for\s+\d+[hms](\s*\d+[hms])*/i;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Patterns indicating Claude is ready for input (legacy fallback).
|
|
|
|
|
|
* Used as secondary signals, not primary detection.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const PROMPT_PATTERNS = [
|
|
|
|
|
|
'❯', // Standard prompt
|
|
|
|
|
|
'\u276f', // Unicode variant
|
|
|
|
|
|
'⏵', // Claude Code prompt variant
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Patterns indicating Claude is actively working.
|
|
|
|
|
|
* When detected, resets all idle detection timers.
|
|
|
|
|
|
* Note: ✻ and ✽ removed - they appear in completion messages too.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const WORKING_PATTERNS = [
|
|
|
|
|
|
'Thinking',
|
|
|
|
|
|
'Writing',
|
|
|
|
|
|
'Reading',
|
|
|
|
|
|
'Running',
|
|
|
|
|
|
'Searching',
|
|
|
|
|
|
'Editing',
|
|
|
|
|
|
'Creating',
|
|
|
|
|
|
'Deleting',
|
|
|
|
|
|
'Analyzing',
|
|
|
|
|
|
'Executing',
|
|
|
|
|
|
'Synthesizing',
|
|
|
|
|
|
'Brewing', // Claude's processing indicators
|
|
|
|
|
|
'Compiling',
|
|
|
|
|
|
'Building',
|
|
|
|
|
|
'Installing',
|
|
|
|
|
|
'Fetching',
|
|
|
|
|
|
'Downloading',
|
|
|
|
|
|
'Processing',
|
|
|
|
|
|
'Generating',
|
|
|
|
|
|
'Loading',
|
|
|
|
|
|
'Starting',
|
|
|
|
|
|
'Updating',
|
|
|
|
|
|
'Checking',
|
|
|
|
|
|
'Validating',
|
|
|
|
|
|
'Testing',
|
|
|
|
|
|
'Formatting',
|
|
|
|
|
|
'Linting',
|
|
|
|
|
|
'⠋',
|
|
|
|
|
|
'⠙',
|
|
|
|
|
|
'⠹',
|
|
|
|
|
|
'⠸',
|
|
|
|
|
|
'⠼',
|
|
|
|
|
|
'⠴',
|
|
|
|
|
|
'⠦',
|
|
|
|
|
|
'⠧',
|
|
|
|
|
|
'⠇',
|
|
|
|
|
|
'⠏', // Spinner chars
|
|
|
|
|
|
'◐',
|
|
|
|
|
|
'◓',
|
|
|
|
|
|
'◑',
|
|
|
|
|
|
'◒', // Alternative spinners
|
|
|
|
|
|
'⣾',
|
|
|
|
|
|
'⣽',
|
|
|
|
|
|
'⣻',
|
|
|
|
|
|
'⢿',
|
|
|
|
|
|
'⡿',
|
|
|
|
|
|
'⣟',
|
|
|
|
|
|
'⣯',
|
|
|
|
|
|
'⣷', // Braille spinners
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check if data contains a completion message pattern.
|
|
|
|
|
|
* Matches "Worked for Xh Xm Xs" time duration patterns.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param data - Raw terminal output data
|
|
|
|
|
|
* @returns True if completion message pattern is found
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function isCompletionMessage(data: string): boolean {
|
|
|
|
|
|
return COMPLETION_TIME_PATTERN.test(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check if a rolling window of terminal output contains working patterns.
|
|
|
|
|
|
* The rolling window catches patterns split across chunks (e.g., "Thin" + "king").
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param window - Rolling window of recent terminal output (already includes current data)
|
|
|
|
|
|
* @returns True if any working pattern is found in the window
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function hasWorkingPattern(window: string): boolean {
|
2026-08-09 15:31:02 +02:00
|
|
|
|
// Current Claude randomizes the gerund ("Actualizing…", "Finagling…"), so the
|
|
|
|
|
|
// list above catches only a fraction of turns. The live status line's own shape
|
|
|
|
|
|
// (`… (13m 23s · ↓ 47.5k tokens)`) is what identifies the rest. Kept as an
|
|
|
|
|
|
// extra signal rather than a replacement: this window is RAW terminal data, and
|
|
|
|
|
|
// a partial repaint can split the line across chunks.
|
|
|
|
|
|
return CLAUDE_WORKING_LINE_PATTERN.test(window) || WORKING_PATTERNS.some((pattern) => window.includes(pattern));
|
2026-03-01 04:04:35 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Extract token count from data if present.
|
|
|
|
|
|
* Parses patterns like "123.4k tokens" or "1.5M tokens".
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param data - Raw terminal output data
|
|
|
|
|
|
* @returns Parsed token count, or null if no token pattern found
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function extractTokenCount(data: string): number | null {
|
|
|
|
|
|
const match = data.match(TOKEN_PATTERN);
|
|
|
|
|
|
if (!match) return null;
|
|
|
|
|
|
|
|
|
|
|
|
let count = parseFloat(match[1]);
|
|
|
|
|
|
const suffix = match[2]?.toLowerCase();
|
|
|
|
|
|
if (suffix === 'k') count *= 1000;
|
|
|
|
|
|
else if (suffix === 'm') count *= 1000000;
|
|
|
|
|
|
|
|
|
|
|
|
return Math.round(count);
|
|
|
|
|
|
}
|