mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
chore: bump version to 0.1438
This commit is contained in:
@@ -508,7 +508,15 @@ export abstract class AiCheckerBase<
|
||||
if (this.consecutiveErrors >= this.config.maxConsecutiveErrors) {
|
||||
this.disable(`${this.config.maxConsecutiveErrors} consecutive errors: ${errorMsg}`);
|
||||
} else {
|
||||
this.startCooldown(this.config.errorCooldownMs);
|
||||
// P1-005: Exponential backoff for errors
|
||||
// Base cooldown * 2^(consecutiveErrors-1), capped at 5 minutes
|
||||
const backoffMultiplier = Math.pow(2, this.consecutiveErrors - 1);
|
||||
const backoffCooldownMs = Math.min(
|
||||
this.config.errorCooldownMs * backoffMultiplier,
|
||||
5 * 60 * 1000 // Max 5 minutes
|
||||
);
|
||||
this.log(`Exponential backoff: ${Math.round(backoffCooldownMs / 1000)}s (error #${this.consecutiveErrors})`);
|
||||
this.startCooldown(backoffCooldownMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+52
-19
@@ -70,33 +70,66 @@ const DEFAULT_AI_CHECK_CONFIG: AiIdleCheckConfig = {
|
||||
/** Pattern to match IDLE or WORKING as the first word of output */
|
||||
const VERDICT_PATTERN = /^\s*(IDLE|WORKING)\b/i;
|
||||
|
||||
/** The prompt sent to the AI checker */
|
||||
const AI_CHECK_PROMPT = `Analyze this terminal output from a running Claude Code session. Determine if the session is IDLE (done working, waiting for new input) or WORKING (still actively processing).
|
||||
/**
|
||||
* The prompt sent to the AI idle checker.
|
||||
*
|
||||
* P1-005: Enhanced with more specific working pattern examples and clearer structure.
|
||||
*/
|
||||
const AI_CHECK_PROMPT = `You are analyzing terminal output from a Claude Code CLI session. Determine if Claude has FINISHED working (IDLE) or is STILL WORKING (WORKING).
|
||||
|
||||
IMPORTANT: When in doubt, answer WORKING. Brief pauses between tool executions do NOT mean the session is idle. Claude may be processing or about to output more.
|
||||
CRITICAL RULE: When in doubt, ALWAYS answer WORKING. False positives (saying IDLE when Claude is working) cause session interruptions. It's safer to wait longer than to interrupt active work.
|
||||
|
||||
IDLE indicators (need MULTIPLE of these to confirm idle):
|
||||
- Completion summary shown (e.g., "✻ Worked for 2m 46s", "Worked for 5s")
|
||||
- Prompt character visible at the end (❯ or similar)
|
||||
- Cost summary displayed (e.g., "$0.12 spent")
|
||||
- Clear end of output with no pending work
|
||||
## IDLE Indicators (need AT LEAST 2 of these together)
|
||||
|
||||
WORKING indicators (ANY of these means WORKING):
|
||||
- Spinner characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ or similar)
|
||||
- Activity text: Thinking, Writing, Reading, Running, Searching, Editing, Creating, Deleting, Analyzing, Executing, Synthesizing, Compiling, Building, Processing, Loading, Generating, Testing, Checking, Validating
|
||||
- Tool execution in progress (commands being run)
|
||||
- Truncated or partial lines at the end
|
||||
- File operations in progress
|
||||
- Output that appears mid-stream or incomplete
|
||||
- No completion summary visible yet
|
||||
1. **Completion Summary** - The most reliable signal:
|
||||
- "✻ Worked for Xm Ys" (e.g., "✻ Worked for 2m 46s")
|
||||
- "Worked for Xs" (e.g., "Worked for 5s")
|
||||
- Cost summary: "$X.XX spent" or "X tokens used"
|
||||
|
||||
Terminal output (most recent at bottom):
|
||||
2. **Input Prompt Visible**:
|
||||
- The ❯ prompt character at the very end
|
||||
- Empty line after completion summary
|
||||
- Waiting cursor position
|
||||
|
||||
3. **Task Completion Language**:
|
||||
- "All done", "Finished", "Completed successfully"
|
||||
- Explicit "waiting for input" or similar
|
||||
|
||||
## WORKING Indicators (ANY ONE of these = answer WORKING)
|
||||
|
||||
### Active Processing Indicators:
|
||||
- **Spinners**: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ (Braille), ◐ ◓ ◑ ◒ (quarter), ⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷
|
||||
- **Activity Words**: Thinking, Writing, Reading, Running, Searching, Editing, Creating, Deleting, Analyzing, Executing, Synthesizing, Compiling, Building, Processing, Loading, Generating, Testing, Checking, Validating, Brewing, Formatting, Linting, Installing, Fetching, Downloading
|
||||
|
||||
### Tool Execution in Progress:
|
||||
- Bash commands with no result shown yet
|
||||
- "Running: npm test", "Executing command..."
|
||||
- File read/write operations incomplete
|
||||
- Progress bars or percentage indicators
|
||||
- Test suite running (dots appearing, "Test Suites: X passed")
|
||||
|
||||
### Output Structure Issues:
|
||||
- Truncated lines without completion
|
||||
- JSON/code blocks not closed
|
||||
- Multi-line output clearly incomplete
|
||||
- "..." indicating more to come
|
||||
- Output ending mid-sentence or mid-word
|
||||
|
||||
### Claude Planning/Thinking:
|
||||
- "Let me...", "I'll...", "Now I need to..."
|
||||
- TodoWrite updates without completion
|
||||
- Plan mode approval prompts (numbered options)
|
||||
|
||||
## Terminal Output to Analyze
|
||||
---
|
||||
{TERMINAL_BUFFER}
|
||||
---
|
||||
|
||||
Answer with EXACTLY one word on the first line: IDLE or WORKING
|
||||
If uncertain, answer WORKING. Then briefly explain why.`;
|
||||
## Your Response
|
||||
First line: EXACTLY "IDLE" or "WORKING" (nothing else)
|
||||
Second line onwards: Brief explanation of your reasoning.
|
||||
|
||||
Remember: When uncertain, answer WORKING.`;
|
||||
|
||||
// ========== AiIdleChecker Class ==========
|
||||
|
||||
|
||||
+1072
-64
File diff suppressed because it is too large
Load Diff
+896
-16
File diff suppressed because it is too large
Load Diff
@@ -727,6 +727,7 @@ export class Session extends EventEmitter {
|
||||
inputTokens: this._totalInputTokens,
|
||||
outputTokens: this._totalOutputTokens,
|
||||
ralphEnabled: this._ralphTracker.enabled,
|
||||
ralphAutoEnableDisabled: this._ralphTracker.autoEnableDisabled || undefined,
|
||||
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
|
||||
parentAgentId: this._parentAgentId || undefined,
|
||||
childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined,
|
||||
|
||||
+214
-1
@@ -166,6 +166,8 @@ export interface SessionState {
|
||||
respawnConfig?: RespawnConfig & { durationMinutes?: number };
|
||||
/** Ralph / Todo tracker enabled */
|
||||
ralphEnabled?: boolean;
|
||||
/** Ralph auto-enable disabled (user explicitly turned off Ralph) */
|
||||
ralphAutoEnableDisabled?: boolean;
|
||||
/** Ralph completion phrase (if set) */
|
||||
ralphCompletionPhrase?: string;
|
||||
/** Parent agent ID if this session is a spawned agent */
|
||||
@@ -394,6 +396,159 @@ export interface RespawnConfig {
|
||||
aiPlanCheckTimeoutMs?: number;
|
||||
/** Cooldown after NOT_PLAN_MODE verdict in ms */
|
||||
aiPlanCheckCooldownMs?: number;
|
||||
|
||||
// ========== P2-001: Adaptive Timing ==========
|
||||
|
||||
/** Whether to use adaptive timing based on historical patterns */
|
||||
adaptiveTimingEnabled?: boolean;
|
||||
/** Minimum value for adaptive completion confirm (ms) */
|
||||
adaptiveMinConfirmMs?: number;
|
||||
/** Maximum value for adaptive completion confirm (ms) */
|
||||
adaptiveMaxConfirmMs?: number;
|
||||
|
||||
// ========== P2-002: Skip-Clear Optimization ==========
|
||||
|
||||
/** Whether to skip /clear when context is below threshold */
|
||||
skipClearWhenLowContext?: boolean;
|
||||
/** Token percentage threshold below which /clear is skipped (0-100) */
|
||||
skipClearThresholdPercent?: number;
|
||||
|
||||
// ========== P2-004: Cycle Metrics ==========
|
||||
|
||||
/** Whether to track and persist cycle metrics */
|
||||
trackCycleMetrics?: boolean;
|
||||
}
|
||||
|
||||
// ========== P2-004: Respawn Cycle Metrics ==========
|
||||
|
||||
/**
|
||||
* Outcome of a respawn cycle
|
||||
*/
|
||||
export type CycleOutcome =
|
||||
| 'success' // Cycle completed normally
|
||||
| 'stuck_recovery' // Stuck-state recovery triggered
|
||||
| 'blocked' // Blocked by circuit breaker or exit signal
|
||||
| 'error' // Error during cycle
|
||||
| 'cancelled'; // Cancelled (e.g., controller stopped)
|
||||
|
||||
/**
|
||||
* Metrics for a single respawn cycle.
|
||||
* Persisted for post-mortem analysis of long-running loops.
|
||||
*/
|
||||
export interface RespawnCycleMetrics {
|
||||
/** Unique cycle ID (session-id:cycle-number) */
|
||||
cycleId: string;
|
||||
/** Session ID this cycle belongs to */
|
||||
sessionId: string;
|
||||
/** Cycle number within the session */
|
||||
cycleNumber: number;
|
||||
/** Timestamp when cycle started */
|
||||
startedAt: number;
|
||||
/** Timestamp when cycle completed */
|
||||
completedAt: number;
|
||||
/** Total duration of cycle (ms) */
|
||||
durationMs: number;
|
||||
/** What triggered idle detection */
|
||||
idleReason: string;
|
||||
/** Time spent detecting idle (from start of watching to idle confirmed) */
|
||||
idleDetectionMs: number;
|
||||
/** Steps completed in this cycle */
|
||||
stepsCompleted: string[];
|
||||
/** Whether /clear was skipped (P2-002) */
|
||||
clearSkipped: boolean;
|
||||
/** Outcome of the cycle */
|
||||
outcome: CycleOutcome;
|
||||
/** Error message if outcome is 'error' */
|
||||
errorMessage?: string;
|
||||
/** Token count at start of cycle */
|
||||
tokenCountAtStart?: number;
|
||||
/** Token count at end of cycle */
|
||||
tokenCountAtEnd?: number;
|
||||
/** Completion confirm time used (may be adaptive) */
|
||||
completionConfirmMsUsed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate metrics across multiple cycles for health scoring.
|
||||
*/
|
||||
export interface RespawnAggregateMetrics {
|
||||
/** Total cycles tracked */
|
||||
totalCycles: number;
|
||||
/** Successful cycles */
|
||||
successfulCycles: number;
|
||||
/** Cycles that required stuck-state recovery */
|
||||
stuckRecoveryCycles: number;
|
||||
/** Blocked cycles */
|
||||
blockedCycles: number;
|
||||
/** Error cycles */
|
||||
errorCycles: number;
|
||||
/** Average cycle duration (ms) */
|
||||
avgCycleDurationMs: number;
|
||||
/** Average idle detection time (ms) */
|
||||
avgIdleDetectionMs: number;
|
||||
/** 90th percentile cycle duration (ms) */
|
||||
p90CycleDurationMs: number;
|
||||
/** Success rate (0-100) */
|
||||
successRate: number;
|
||||
/** Last updated timestamp */
|
||||
lastUpdatedAt: number;
|
||||
}
|
||||
|
||||
// ========== P2-005: Ralph Loop Health Score ==========
|
||||
|
||||
/**
|
||||
* Health status levels for the Ralph Loop system.
|
||||
*/
|
||||
export type HealthStatus = 'excellent' | 'good' | 'degraded' | 'critical';
|
||||
|
||||
/**
|
||||
* Comprehensive health score for a Ralph Loop session.
|
||||
* Aggregates multiple health signals into a single score.
|
||||
*/
|
||||
export interface RalphLoopHealthScore {
|
||||
/** Overall health score (0-100) */
|
||||
score: number;
|
||||
/** Health status based on score thresholds */
|
||||
status: HealthStatus;
|
||||
/** Individual component scores (0-100 each) */
|
||||
components: {
|
||||
/** Based on recent cycle success rate */
|
||||
cycleSuccess: number;
|
||||
/** Based on circuit breaker state */
|
||||
circuitBreaker: number;
|
||||
/** Based on iteration stall metrics */
|
||||
iterationProgress: number;
|
||||
/** Based on AI checker error rate */
|
||||
aiChecker: number;
|
||||
/** Based on stuck-state recovery count */
|
||||
stuckRecovery: number;
|
||||
};
|
||||
/** Human-readable summary of health */
|
||||
summary: string;
|
||||
/** Recommendations for improvement */
|
||||
recommendations: string[];
|
||||
/** Timestamp when score was calculated */
|
||||
calculatedAt: number;
|
||||
}
|
||||
|
||||
// ========== Timing History for Adaptive Timing ==========
|
||||
|
||||
/**
|
||||
* Historical timing data for adaptive adjustments.
|
||||
*/
|
||||
export interface TimingHistory {
|
||||
/** Rolling window of recent idle detection durations (ms) */
|
||||
recentIdleDetectionMs: number[];
|
||||
/** Rolling window of recent cycle durations (ms) */
|
||||
recentCycleDurationMs: number[];
|
||||
/** Calculated adaptive completion confirm value (ms) */
|
||||
adaptiveCompletionConfirmMs: number;
|
||||
/** Number of samples in rolling windows */
|
||||
sampleCount: number;
|
||||
/** Maximum samples to keep */
|
||||
maxSamples: number;
|
||||
/** Last updated timestamp */
|
||||
lastUpdatedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -829,13 +984,43 @@ export type RalphTodoStatus = 'pending' | 'in_progress' | 'completed';
|
||||
/**
|
||||
* State of per-session Ralph / Todo tracking (detected from Claude output)
|
||||
*/
|
||||
/**
|
||||
* Confidence scoring for completion detection.
|
||||
* Helps distinguish genuine completion signals from false positives.
|
||||
*/
|
||||
export interface CompletionConfidence {
|
||||
/** Overall confidence level (0-100) */
|
||||
score: number;
|
||||
/** Whether score is above threshold for triggering completion */
|
||||
isConfident: boolean;
|
||||
/** Individual signal contributions */
|
||||
signals: {
|
||||
/** Promise tag detected with proper formatting */
|
||||
hasPromiseTag: boolean;
|
||||
/** Phrase matches expected completion phrase */
|
||||
matchesExpected: boolean;
|
||||
/** All todos are marked complete */
|
||||
allTodosComplete: boolean;
|
||||
/** EXIT_SIGNAL: true in RALPH_STATUS block */
|
||||
hasExitSignal: boolean;
|
||||
/** Multiple completion indicators present */
|
||||
multipleIndicators: boolean;
|
||||
/** Output context suggests completion (not in prompt/explanation) */
|
||||
contextAppropriate: boolean;
|
||||
};
|
||||
/** Timestamp of last confidence calculation */
|
||||
calculatedAt: number;
|
||||
}
|
||||
|
||||
export interface RalphTrackerState {
|
||||
/** Whether the tracker is actively monitoring (disabled by default) */
|
||||
enabled: boolean;
|
||||
/** Whether a loop is currently active */
|
||||
active: boolean;
|
||||
/** Detected completion phrase */
|
||||
/** Detected completion phrase (primary) */
|
||||
completionPhrase: string | null;
|
||||
/** Additional valid completion phrases (P1-003: multi-phrase support) */
|
||||
alternateCompletionPhrases?: string[];
|
||||
/** Timestamp when loop started */
|
||||
startedAt: number | null;
|
||||
/** Number of cycles/iterations detected */
|
||||
@@ -850,6 +1035,8 @@ export interface RalphTrackerState {
|
||||
planVersion?: number;
|
||||
/** Number of versions in history (for versioning UI) */
|
||||
planHistoryLength?: number;
|
||||
/** Last completion confidence assessment */
|
||||
completionConfidence?: CompletionConfidence;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -872,6 +1059,32 @@ export interface RalphTodoItem {
|
||||
detectedAt: number;
|
||||
/** Priority level (P0=critical, P1=high, P2=normal) */
|
||||
priority: RalphTodoPriority;
|
||||
/** P1-009: Estimated time to complete (ms), based on historical patterns */
|
||||
estimatedDurationMs?: number;
|
||||
/** P1-009: Complexity category for progress estimation */
|
||||
estimatedComplexity?: 'trivial' | 'simple' | 'moderate' | 'complex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress estimation for the todo list
|
||||
*/
|
||||
export interface RalphTodoProgress {
|
||||
/** Total number of todos */
|
||||
total: number;
|
||||
/** Number completed */
|
||||
completed: number;
|
||||
/** Number in progress */
|
||||
inProgress: number;
|
||||
/** Number pending */
|
||||
pending: number;
|
||||
/** Completion percentage (0-100) */
|
||||
percentComplete: number;
|
||||
/** Estimated remaining time (ms), based on historical completion rate */
|
||||
estimatedRemainingMs: number | null;
|
||||
/** Average time per todo completion (ms) */
|
||||
avgCompletionTimeMs: number | null;
|
||||
/** Projected completion timestamp (epoch ms) */
|
||||
projectedCompletionAt: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,3 +24,12 @@ export {
|
||||
validateTokenCounts,
|
||||
validateTokensAndCost,
|
||||
} from './token-validation.js';
|
||||
export {
|
||||
levenshteinDistance,
|
||||
stringSimilarity,
|
||||
isSimilar,
|
||||
isSimilarByDistance,
|
||||
normalizePhrase,
|
||||
fuzzyPhraseMatch,
|
||||
todoContentHash,
|
||||
} from './string-similarity.js';
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @fileoverview String similarity utilities for fuzzy matching.
|
||||
*
|
||||
* Provides Levenshtein distance and similarity scoring for:
|
||||
* - Completion phrase fuzzy matching
|
||||
* - Todo content deduplication
|
||||
*
|
||||
* @module utils/string-similarity
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate the Levenshtein (edit) distance between two strings.
|
||||
* This is the minimum number of single-character edits (insertions,
|
||||
* deletions, or substitutions) required to change one string into the other.
|
||||
*
|
||||
* Uses Wagner-Fischer algorithm with O(min(m,n)) space optimization.
|
||||
*
|
||||
* @param a - First string
|
||||
* @param b - Second string
|
||||
* @returns The edit distance (0 = identical)
|
||||
*
|
||||
* @example
|
||||
* levenshteinDistance('hello', 'hello') // 0
|
||||
* levenshteinDistance('hello', 'helo') // 1 (one deletion)
|
||||
* levenshteinDistance('COMPLETE', 'COMPLET') // 1 (one deletion)
|
||||
*/
|
||||
export function levenshteinDistance(a: string, b: string): number {
|
||||
// Ensure a is the shorter string for space efficiency
|
||||
if (a.length > b.length) {
|
||||
[a, b] = [b, a];
|
||||
}
|
||||
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
|
||||
// Early exit for identical strings
|
||||
if (a === b) return 0;
|
||||
|
||||
// Early exit for empty strings
|
||||
if (m === 0) return n;
|
||||
|
||||
// Use single array (space optimization)
|
||||
let prev = new Array<number>(m + 1);
|
||||
let curr = new Array<number>(m + 1);
|
||||
|
||||
// Initialize first row
|
||||
for (let i = 0; i <= m; i++) {
|
||||
prev[i] = i;
|
||||
}
|
||||
|
||||
// Fill the matrix row by row
|
||||
for (let j = 1; j <= n; j++) {
|
||||
curr[0] = j;
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
curr[i] = Math.min(
|
||||
prev[i] + 1, // deletion
|
||||
curr[i - 1] + 1, // insertion
|
||||
prev[i - 1] + cost // substitution
|
||||
);
|
||||
}
|
||||
|
||||
// Swap rows
|
||||
[prev, curr] = [curr, prev];
|
||||
}
|
||||
|
||||
return prev[m];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate similarity ratio between two strings (0 to 1).
|
||||
* Uses Levenshtein distance normalized by the longer string's length.
|
||||
*
|
||||
* @param a - First string
|
||||
* @param b - Second string
|
||||
* @returns Similarity ratio (1.0 = identical, 0.0 = completely different)
|
||||
*
|
||||
* @example
|
||||
* stringSimilarity('hello', 'hello') // 1.0
|
||||
* stringSimilarity('hello', 'helo') // 0.8 (4/5 similar)
|
||||
* stringSimilarity('abc', 'xyz') // 0.0 (3 edits, length 3)
|
||||
*/
|
||||
export function stringSimilarity(a: string, b: string): number {
|
||||
if (a === b) return 1.0;
|
||||
if (a.length === 0 && b.length === 0) return 1.0;
|
||||
if (a.length === 0 || b.length === 0) return 0.0;
|
||||
|
||||
const distance = levenshteinDistance(a, b);
|
||||
const maxLength = Math.max(a.length, b.length);
|
||||
return 1 - distance / maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two strings are similar within a given threshold.
|
||||
*
|
||||
* @param a - First string
|
||||
* @param b - Second string
|
||||
* @param threshold - Minimum similarity ratio (default: 0.85 = 85% similar)
|
||||
* @returns True if similarity >= threshold
|
||||
*
|
||||
* @example
|
||||
* isSimilar('COMPLETE', 'COMPLET', 0.85) // true (87.5% similar)
|
||||
* isSimilar('COMPLETE', 'DONE', 0.85) // false (0% similar)
|
||||
*/
|
||||
export function isSimilar(a: string, b: string, threshold = 0.85): boolean {
|
||||
return stringSimilarity(a, b) >= threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two strings are similar with edit distance tolerance.
|
||||
* More intuitive for short strings than percentage-based threshold.
|
||||
*
|
||||
* @param a - First string
|
||||
* @param b - Second string
|
||||
* @param maxDistance - Maximum allowed edit distance (default: 2)
|
||||
* @returns True if edit distance <= maxDistance
|
||||
*
|
||||
* @example
|
||||
* isSimilarByDistance('COMPLETE', 'COMPLET', 2) // true (distance 1)
|
||||
* isSimilarByDistance('COMPLETE', 'COMP', 2) // false (distance 4)
|
||||
*/
|
||||
export function isSimilarByDistance(a: string, b: string, maxDistance = 2): boolean {
|
||||
return levenshteinDistance(a, b) <= maxDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a completion phrase for comparison.
|
||||
* Handles variations in case, whitespace, and separators.
|
||||
*
|
||||
* @param phrase - Raw completion phrase
|
||||
* @returns Normalized phrase (uppercase, no separators)
|
||||
*
|
||||
* @example
|
||||
* normalizePhrase('task_done') // 'TASKDONE'
|
||||
* normalizePhrase('TASK-DONE') // 'TASKDONE'
|
||||
* normalizePhrase('Task Done') // 'TASKDONE'
|
||||
*/
|
||||
export function normalizePhrase(phrase: string): string {
|
||||
return phrase
|
||||
.toUpperCase()
|
||||
.replace(/[\s_\-\.]+/g, '') // Remove whitespace, underscores, hyphens, dots
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two completion phrases match with fuzzy tolerance.
|
||||
*
|
||||
* First normalizes both phrases, then checks:
|
||||
* 1. Exact match after normalization
|
||||
* 2. Edit distance <= maxDistance for typo tolerance
|
||||
*
|
||||
* @param phrase1 - First phrase to compare
|
||||
* @param phrase2 - Second phrase to compare
|
||||
* @param maxDistance - Maximum edit distance for fuzzy match (default: 2)
|
||||
* @returns True if phrases match (exact or fuzzy)
|
||||
*
|
||||
* @example
|
||||
* fuzzyPhraseMatch('COMPLETE', 'COMPLETE') // true (exact)
|
||||
* fuzzyPhraseMatch('COMPLETE', 'COMPLET') // true (typo)
|
||||
* fuzzyPhraseMatch('TASK_DONE', 'TASKDONE') // true (separator)
|
||||
* fuzzyPhraseMatch('COMPLETE', 'FINISHED') // false (different word)
|
||||
*/
|
||||
export function fuzzyPhraseMatch(
|
||||
phrase1: string,
|
||||
phrase2: string,
|
||||
maxDistance = 2
|
||||
): boolean {
|
||||
const norm1 = normalizePhrase(phrase1);
|
||||
const norm2 = normalizePhrase(phrase2);
|
||||
|
||||
// Exact match after normalization
|
||||
if (norm1 === norm2) return true;
|
||||
|
||||
// For short phrases (< 6 chars), require exact match to avoid false positives
|
||||
// e.g., "DONE" shouldn't match "DENY"
|
||||
if (norm1.length < 6 || norm2.length < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fuzzy match with edit distance
|
||||
return isSimilarByDistance(norm1, norm2, maxDistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a content hash for todo deduplication.
|
||||
* Normalizes content and generates a simple hash.
|
||||
*
|
||||
* @param content - Todo item content
|
||||
* @returns Normalized hash string for comparison
|
||||
*/
|
||||
export function todoContentHash(content: string): string {
|
||||
// Normalize: lowercase, collapse whitespace, remove punctuation
|
||||
const normalized = content
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/[^\w\s]/g, '')
|
||||
.trim();
|
||||
|
||||
// Simple hash using reduce (fast, good enough for deduplication)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < normalized.length; i++) {
|
||||
const char = normalized.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
+29
-17
@@ -6504,8 +6504,14 @@ class ClaudemanApp {
|
||||
if (total > 0) {
|
||||
tokensEl.style.display = '';
|
||||
const tokenStr = this.formatTokens(total);
|
||||
const estimatedCost = this.estimateCost(input, output);
|
||||
tokensEl.textContent = `${tokenStr} tokens · $${estimatedCost.toFixed(2)}`;
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const showCost = settings.showCost ?? false;
|
||||
if (showCost) {
|
||||
const estimatedCost = this.estimateCost(input, output);
|
||||
tokensEl.textContent = `${tokenStr} tokens · $${estimatedCost.toFixed(2)}`;
|
||||
} else {
|
||||
tokensEl.textContent = `${tokenStr} tokens`;
|
||||
}
|
||||
} else {
|
||||
tokensEl.style.display = 'none';
|
||||
}
|
||||
@@ -6865,10 +6871,14 @@ class ClaudemanApp {
|
||||
const estimatedCost = this.estimateCost(totalInput, totalOutput);
|
||||
const tokenEl = this.$('headerTokens');
|
||||
if (tokenEl) {
|
||||
tokenEl.textContent = total > 0 ? `${display} tokens · $${estimatedCost.toFixed(2)}` : '0 tokens';
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const showCost = settings.showCost ?? false;
|
||||
tokenEl.textContent = total > 0
|
||||
? (showCost ? `${display} tokens · $${estimatedCost.toFixed(2)}` : `${display} tokens`)
|
||||
: '0 tokens';
|
||||
tokenEl.title = this.globalStats
|
||||
? `Lifetime: ${this.globalStats.totalSessionsCreated} sessions created\nEstimated cost based on Claude Opus pricing`
|
||||
: 'Token usage across active sessions\nEstimated cost based on Claude Opus pricing';
|
||||
? `Lifetime: ${this.globalStats.totalSessionsCreated} sessions created${showCost ? '\nEstimated cost based on Claude Opus pricing' : ''}`
|
||||
: `Token usage across active sessions${showCost ? '\nEstimated cost based on Claude Opus pricing' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7781,16 +7791,17 @@ class ClaudemanApp {
|
||||
document.getElementById('appSettingsDefaultDir').value = settings.defaultWorkingDir || '';
|
||||
document.getElementById('appSettingsRalphEnabled').checked = settings.ralphTrackerEnabled ?? false;
|
||||
// Header visibility settings (default to true/enabled)
|
||||
document.getElementById('appSettingsShowFontControls').checked = settings.showFontControls ?? true;
|
||||
document.getElementById('appSettingsShowFontControls').checked = settings.showFontControls ?? false;
|
||||
document.getElementById('appSettingsShowSystemStats').checked = settings.showSystemStats ?? true;
|
||||
document.getElementById('appSettingsShowTokenCount').checked = settings.showTokenCount ?? true;
|
||||
document.getElementById('appSettingsShowCost').checked = settings.showCost ?? false;
|
||||
document.getElementById('appSettingsShowMonitor').checked = settings.showMonitor ?? true;
|
||||
document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? true;
|
||||
document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? false;
|
||||
document.getElementById('appSettingsShowFileBrowser').checked = settings.showFileBrowser ?? false;
|
||||
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? true;
|
||||
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? false;
|
||||
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? true;
|
||||
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? false;
|
||||
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? true;
|
||||
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? true;
|
||||
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? false;
|
||||
// Claude CLI settings
|
||||
const claudeModeSelect = document.getElementById('appSettingsClaudeMode');
|
||||
const allowedToolsRow = document.getElementById('allowedToolsRow');
|
||||
@@ -7906,6 +7917,7 @@ class ClaudemanApp {
|
||||
showFontControls: document.getElementById('appSettingsShowFontControls').checked,
|
||||
showSystemStats: document.getElementById('appSettingsShowSystemStats').checked,
|
||||
showTokenCount: document.getElementById('appSettingsShowTokenCount').checked,
|
||||
showCost: document.getElementById('appSettingsShowCost').checked,
|
||||
showMonitor: document.getElementById('appSettingsShowMonitor').checked,
|
||||
showProjectInsights: document.getElementById('appSettingsShowProjectInsights').checked,
|
||||
showFileBrowser: document.getElementById('appSettingsShowFileBrowser').checked,
|
||||
@@ -8107,7 +8119,7 @@ class ClaudemanApp {
|
||||
applyHeaderVisibilitySettings() {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
// Default all to true (enabled) if not set
|
||||
const showFontControls = settings.showFontControls ?? true;
|
||||
const showFontControls = settings.showFontControls ?? false;
|
||||
const showSystemStats = settings.showSystemStats ?? true;
|
||||
const showTokenCount = settings.showTokenCount ?? true;
|
||||
|
||||
@@ -8141,7 +8153,7 @@ class ClaudemanApp {
|
||||
applyMonitorVisibility() {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const showMonitor = settings.showMonitor ?? true;
|
||||
const showSubagents = settings.showSubagents ?? true;
|
||||
const showSubagents = settings.showSubagents ?? false;
|
||||
const showFileBrowser = settings.showFileBrowser ?? false;
|
||||
|
||||
const monitorPanel = document.getElementById('monitorPanel');
|
||||
@@ -10679,7 +10691,7 @@ class ClaudemanApp {
|
||||
*/
|
||||
updateSubagentWindowVisibility() {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
|
||||
|
||||
for (const [agentId, windowInfo] of this.subagentWindows) {
|
||||
// Get parent from PERSISTENT map (THE source of truth)
|
||||
@@ -10722,7 +10734,7 @@ class ClaudemanApp {
|
||||
const existing = this.subagentWindows.get(agentId);
|
||||
const agent = this.subagents.get(agentId);
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
|
||||
|
||||
// If window is hidden (different tab) and activeTabOnly is enabled, switch to parent tab
|
||||
if (existing.hidden && agent?.parentSessionId && activeTabOnly) {
|
||||
@@ -10883,7 +10895,7 @@ class ClaudemanApp {
|
||||
// Check if this window should be visible based on settings
|
||||
// Use the PERSISTENT parent map for accurate tab-based visibility
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
|
||||
let shouldHide = false;
|
||||
if (activeTabOnly) {
|
||||
const storedParent = this.subagentParentMap.get(agentId);
|
||||
@@ -11132,7 +11144,7 @@ class ClaudemanApp {
|
||||
|
||||
if (windowData) {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? false;
|
||||
const activeTabOnly = settings.subagentActiveTabOnly ?? true;
|
||||
|
||||
// Get parent from PERSISTENT map (THE source of truth)
|
||||
const storedParent = this.subagentParentMap.get(agentId);
|
||||
@@ -11559,7 +11571,7 @@ class ClaudemanApp {
|
||||
|
||||
// Check if panel is enabled in settings
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const showProjectInsights = settings.showProjectInsights ?? true;
|
||||
const showProjectInsights = settings.showProjectInsights ?? false;
|
||||
if (!showProjectInsights) {
|
||||
panel.classList.remove('visible');
|
||||
this.projectInsightsPanelVisible = false;
|
||||
|
||||
@@ -731,6 +731,13 @@
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-item" title="Show estimated cost next to token count">
|
||||
<span class="settings-item-label">Show Cost ($)</span>
|
||||
<label class="switch switch-sm">
|
||||
<input type="checkbox" id="appSettingsShowCost" checked>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Panels Section -->
|
||||
<div class="settings-section-header">Panels</div>
|
||||
|
||||
+24
-20
@@ -5118,22 +5118,24 @@ kbd {
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
stroke: #00ff41;
|
||||
stroke-width: 2.5;
|
||||
stroke: #3b82f6;
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 5 3;
|
||||
fill: none;
|
||||
opacity: 0.75;
|
||||
/* Dark outline for contrast, subtle glow */
|
||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.7))
|
||||
drop-shadow(0 0 3px rgba(0, 255, 65, 0.5));
|
||||
opacity: 0.9;
|
||||
/* Dark outline for contrast, vibrant blue glow */
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
|
||||
drop-shadow(0 0 4px rgba(59, 130, 246, 0.8))
|
||||
drop-shadow(0 0 8px rgba(59, 130, 246, 0.5));
|
||||
transition: opacity 0.2s, stroke-width 0.2s, filter 0.2s;
|
||||
}
|
||||
|
||||
.connection-line:hover {
|
||||
opacity: 0.9;
|
||||
stroke-width: 3;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
|
||||
drop-shadow(0 0 5px rgba(0, 255, 65, 0.7));
|
||||
opacity: 1;
|
||||
stroke-width: 3.5;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9))
|
||||
drop-shadow(0 0 6px rgba(59, 130, 246, 1))
|
||||
drop-shadow(0 0 12px rgba(59, 130, 246, 0.7));
|
||||
}
|
||||
|
||||
.connection-line.spawning-line {
|
||||
@@ -5160,24 +5162,26 @@ kbd {
|
||||
|
||||
/* Plan subagent to regular subagent connection lines (Opus → Haiku) */
|
||||
.connection-line.plan-to-subagent-line {
|
||||
stroke: #00ff41;
|
||||
stroke-width: 2.5;
|
||||
/* Dark outline for contrast, subtle glow */
|
||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.7))
|
||||
drop-shadow(0 0 3px rgba(0, 255, 65, 0.5));
|
||||
stroke: #3b82f6;
|
||||
stroke-width: 3;
|
||||
/* Dark outline for contrast, vibrant blue glow */
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
|
||||
drop-shadow(0 0 4px rgba(59, 130, 246, 0.8))
|
||||
drop-shadow(0 0 8px rgba(59, 130, 246, 0.5));
|
||||
stroke-dasharray: 5 3;
|
||||
animation: plan-subagent-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.connection-line.plan-to-subagent-line:hover {
|
||||
stroke-width: 3;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
|
||||
drop-shadow(0 0 5px rgba(0, 255, 65, 0.7));
|
||||
stroke-width: 3.5;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9))
|
||||
drop-shadow(0 0 6px rgba(59, 130, 246, 1))
|
||||
drop-shadow(0 0 12px rgba(59, 130, 246, 0.7));
|
||||
}
|
||||
|
||||
@keyframes plan-subagent-pulse {
|
||||
0%, 100% { opacity: 0.65; }
|
||||
50% { opacity: 0.85; }
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ========== Project Insights Panel (Bash File Viewers) ========== */
|
||||
|
||||
+18
-6
@@ -1121,8 +1121,12 @@ export class WebServer extends EventEmitter {
|
||||
if (enabled !== undefined) {
|
||||
if (enabled) {
|
||||
session.ralphTracker.enable();
|
||||
// Allow re-enabling on restart if user explicitly enabled
|
||||
session.ralphTracker.enableAutoEnable();
|
||||
} else {
|
||||
session.ralphTracker.disable();
|
||||
// Prevent re-enabling on restart when user explicitly disabled
|
||||
session.ralphTracker.disableAutoEnable();
|
||||
}
|
||||
// Persist Ralph enabled state
|
||||
this.screenManager.updateRalphEnabled(id, enabled);
|
||||
@@ -1369,8 +1373,8 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled)
|
||||
if (this.store.getConfig().ralphEnabled) {
|
||||
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user)
|
||||
if (this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) {
|
||||
autoConfigureRalph(session, session.workingDir, () => {});
|
||||
if (!session.ralphTracker.enabled) {
|
||||
session.ralphTracker.enable();
|
||||
@@ -1688,8 +1692,8 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled)
|
||||
if (this.store.getConfig().ralphEnabled) {
|
||||
// Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user)
|
||||
if (this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) {
|
||||
autoConfigureRalph(session, session.workingDir, () => {});
|
||||
if (!session.ralphTracker.enabled) {
|
||||
session.ralphTracker.enable();
|
||||
@@ -2288,6 +2292,7 @@ export class WebServer extends EventEmitter {
|
||||
autoConfigureRalph(session, casePath, () => {}); // no broadcast yet
|
||||
if (!session.ralphTracker.enabled) {
|
||||
session.ralphTracker.enable();
|
||||
session.ralphTracker.enableAutoEnable(); // Allow re-enabling on restart
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4561,6 +4566,13 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
}
|
||||
// Ralph / Todo tracker
|
||||
if (savedState.ralphAutoEnableDisabled) {
|
||||
session.ralphTracker.disableAutoEnable();
|
||||
console.log(`[Server] Restored Ralph auto-enable disabled for session ${session.id}`);
|
||||
} else if (savedState.ralphEnabled) {
|
||||
// If Ralph was enabled and not explicitly disabled, allow re-enabling on restart
|
||||
session.ralphTracker.enableAutoEnable();
|
||||
}
|
||||
if (savedState.ralphEnabled) {
|
||||
session.ralphTracker.enable();
|
||||
if (savedState.ralphCompletionPhrase) {
|
||||
@@ -4594,8 +4606,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: restore Ralph state from state-inner.json if not already set
|
||||
if (!session.ralphTracker.enabled) {
|
||||
// Fallback: restore Ralph state from state-inner.json if not already set and not explicitly disabled
|
||||
if (!session.ralphTracker.enabled && !session.ralphTracker.autoEnableDisabled) {
|
||||
const ralphState = this.store.getRalphState(screen.sessionId);
|
||||
if (ralphState?.loop?.enabled) {
|
||||
session.ralphTracker.restoreState(ralphState.loop, ralphState.todos);
|
||||
|
||||
Reference in New Issue
Block a user