chore: bump version to 0.1438

This commit is contained in:
arkon
2026-01-30 17:20:53 +01:00
parent f4612278ef
commit 5b6dd078f1
20 changed files with 2757 additions and 182 deletions
+9 -1
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+896 -16
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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
View File
@@ -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;
}
/**
+9
View File
@@ -24,3 +24,12 @@ export {
validateTokenCounts,
validateTokensAndCost,
} from './token-validation.js';
export {
levenshteinDistance,
stringSimilarity,
isSimilar,
isSimilarByDistance,
normalizePhrase,
fuzzyPhraseMatch,
todoContentHash,
} from './string-similarity.js';
+208
View File
@@ -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
View File
@@ -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;
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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);