From 0d1ca37b6e5aa9780a7961d3d270861608fc52f2 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 21 Jan 2026 22:36:45 +0100 Subject: [PATCH] refactor: rename inner-loop-tracker to ralph-tracker with API improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename inner-loop-tracker.ts → ralph-tracker.ts throughout codebase - Add ralph-config.ts for parsing .claude/ralph-loop.local.md config - Standardize API error responses using createErrorResponse() - Add input validation for auto-compact/auto-clear thresholds - Update UI labels to "Ralph / Todo Tracker" consistently - Add 46 integration tests for Ralph tracking functionality - Update test badge to 438 total tests Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 79 +- README.md | 2 +- src/ralph-config.ts | 153 +++ ...inner-loop-tracker.ts => ralph-tracker.ts} | 66 +- src/screen-manager.ts | 6 +- src/session.ts | 92 +- src/state-store.ts | 92 +- src/tui/App.tsx | 12 +- src/tui/components/RalphPanel.tsx | 10 +- src/tui/hooks/useSessionManager.ts | 48 +- src/types.ts | 40 +- src/web/public/app.js | 124 +- src/web/public/index.html | 14 +- src/web/public/styles.css | 4 +- src/web/server.ts | 270 +++-- test/ralph-config.test.ts | 420 +++++++ test/ralph-integration.test.ts | 1025 +++++++++++++++++ ...-tracker.test.ts => ralph-tracker.test.ts} | 68 +- test/session.test.ts | 7 +- test/state-store.test.ts | 54 +- test/types.test.ts | 16 +- 21 files changed, 2151 insertions(+), 451 deletions(-) create mode 100644 src/ralph-config.ts rename src/{inner-loop-tracker.ts => ralph-tracker.ts} (96%) create mode 100644 test/ralph-config.test.ts create mode 100644 test/ralph-integration.test.ts rename test/{inner-loop-tracker.test.ts => ralph-tracker.test.ts} (91%) diff --git a/CLAUDE.md b/CLAUDE.md index c067ea80..2d19f86a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,8 +65,9 @@ npx vitest run -t "should create session" # By pattern # 3110: edge-cases.test.ts # 3115: integration-flows.test.ts # 3120: session-cleanup.test.ts -# Unit tests (no port needed): respawn-controller, inner-loop-tracker, pty-interactive, task-queue, task, ralph-loop, session-manager, state-store, types, templates -# Next available: 3122+ +# 3125: ralph-integration.test.ts +# Unit tests (no port needed): respawn-controller, ralph-tracker, pty-interactive, task-queue, task, ralph-loop, session-manager, state-store, types, templates, ralph-config +# Next available: 3127+ # Tests mock PTY - no real Claude CLI spawned # Test timeout: 30s (configured in vitest.config.ts) @@ -82,7 +83,7 @@ screen -r # Attach to screen session (Ctrl+A D t curl localhost:3000/api/sessions # Check active sessions curl localhost:3000/api/status | jq . # Full app state including respawn cat ~/.claudeman/state.json | jq . # View main state -cat ~/.claudeman/state-inner.json | jq . # View inner loop state +cat ~/.claudeman/state-inner.json | jq . # View Ralph loop state # Kill stuck screen sessions screen -X -S quit # Graceful quit @@ -98,7 +99,8 @@ pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens | `src/session.ts` | Core PTY wrapper for Claude CLI. Modes: `runPrompt()`, `startInteractive()`, `startShell()` | | `src/respawn-controller.ts` | State machine for autonomous session cycling | | `src/screen-manager.ts` | GNU screen persistence, ghost discovery, 4-strategy kill | -| `src/inner-loop-tracker.ts` | Detects `PHRASE`, todos, loop status in output | +| `src/ralph-tracker.ts` | Detects `PHRASE`, todos, loop status in output | +| `src/ralph-config.ts` | Parses `.claude/ralph-loop.local.md` and CLAUDE.md for Ralph config | | `src/task-tracker.ts` | Parses background task output (agent IDs, status) from Claude CLI | | `src/session-manager.ts` | Manages session lifecycle, task assignment, and cleanup | | `src/state-store.ts` | JSON persistence to `~/.claudeman/` with debounced writes | @@ -259,18 +261,35 @@ curl -X POST localhost:3000/api/sessions/:id/input \ Both wait for idle. Configure via `session.setAutoCompact()` / `session.setAutoClear()`. -### Inner Loop Tracking +### Ralph / Todo Tracking Detects Ralph loops and todos inside Claude sessions. **Disabled by default** but auto-enables when any of these patterns are detected in terminal output: - `/ralph-loop:ralph-loop` command -- `PHRASE` completion phrases +- `PHRASE` completion phrases (supports hyphens: `TESTS-PASS`, underscores: `ALL_DONE`, numbers: `TASK_123`) - `TodoWrite` tool usage - Iteration patterns (`Iteration 5/50`, `[5/50]`) - Todo checkboxes (`- [ ]`/`- [x]`) or indicator icons (`☐`/`◐`/`✓`) - "All tasks complete" messages - Individual task completion signals (`Task 8 is done`) -See `inner-loop-tracker.ts:shouldAutoEnable()` for detection logic. +See `ralph-tracker.ts:shouldAutoEnable()` for detection logic. + +**Auto-Configuration from Ralph Plugin State**: When a session starts, Claudeman reads `.claude/ralph-loop.local.md` (the official Ralph Wiggum plugin state file) to auto-configure the tracker: + +```yaml +--- +enabled: true +iteration: 5 +max-iterations: 50 +completion-promise: "COMPLETE" +--- +``` + +Priority order for configuration: +1. `.claude/ralph-loop.local.md` (official Ralph plugin state) +2. `CLAUDE.md` `` tags (fallback) + +See `src/ralph-config.ts` for parsing logic. **Completion Detection**: Uses multi-strategy detection: - 1st occurrence of `PHRASE`: Stores as expected phrase (likely in prompt) @@ -287,8 +306,8 @@ See `inner-loop-tracker.ts:shouldAutoEnable()` for detection logic. - `tracker.fullReset()` → Complete reset to initial state **API**: -- `GET /api/sessions/:id/inner-state` - Get loop state and todos -- `POST /api/sessions/:id/inner-config` - Configure tracker: +- `GET /api/sessions/:id/ralph-state` - Get loop state and todos +- `POST /api/sessions/:id/ralph-config` - Configure tracker: - `{ enabled: boolean }` - Enable/disable - `{ reset: true }` - Soft reset (keep enabled) - `{ reset: "full" }` - Full reset @@ -309,7 +328,7 @@ Key events for frontend handling (see `app.js:handleSSEEvent()`): - `session:idle`, `session:working` - Status indicator updates - `session:terminal`, `session:clearTerminal` - Terminal content - `session:completion`, `session:autoClear`, `session:autoCompact` - Lifecycle events -- `session:innerLoopUpdate`, `session:innerTodoUpdate`, `session:innerCompletionDetected` - Ralph tracking +- `session:ralphLoopUpdate`, `session:ralphTodoUpdate`, `session:ralphCompletionDetected` - Ralph tracking ### Frontend (app.js) @@ -337,9 +356,9 @@ Writes debounced to `~/.claudeman/state.json`. Batches rapid changes. | Terminal batch interval | 16ms | `server.ts` (60fps) | | Output batch interval | 50ms | `server.ts` | | Task update batch interval | 100ms | `server.ts` | -| Inner loop event debounce | 50ms | `inner-loop-tracker.ts` | +| Ralph loop event debounce | 50ms | `ralph-tracker.ts` | | Session tabs render debounce | 100ms | `app.js` | -| Inner panel render debounce | 50ms | `app.js` | +| Ralph panel render debounce | 50ms | `app.js` | | Task panel render debounce | 100ms | `app.js` | | Input batch interval | 16ms | `app.js` (60fps) | | Idle activity timeout | 2s | `session.ts` | @@ -354,8 +373,10 @@ Module resolution: NodeNext. Target: ES2022. Strict mode with additional checks: | `noUnusedLocals` | Error on unused local variables | | `noUnusedParameters` | Error on unused function parameters | | `noImplicitReturns` | All code paths must return a value | +| `noImplicitOverride` | Require `override` keyword for overridden methods | | `noFallthroughCasesInSwitch` | Require break/return in switch cases | | `allowUnreachableCode: false` | Error on unreachable code | +| `allowUnusedLabels: false` | Error on unused labels | TUI uses React JSX (`jsxImportSource: react`) for Ink components. @@ -386,6 +407,32 @@ Use `createErrorResponse(code, details?)` from `types.ts`: - **Ghost discovery**: `reconcileScreens()` finds orphaned screens on startup - **Cleanup** (`cleanupSession()`): stops respawn, clears buffers/timers, kills screen +## TUI Architecture (Ink/React) + +The TUI (`claudeman tui`) is built with [Ink](https://github.com/vadimdemedes/ink) (React for terminals). + +**Component Hierarchy:** +``` +App.tsx +├── StartScreen.tsx # Session/case list, navigation +│ ├── List navigation (↑/↓, Enter, a/d/D) +│ ├── Case creation flow +│ └── Tab switcher menu +├── TabBar.tsx # Session tabs (when attached) +├── TerminalView.tsx # Viewport into screen session +├── StatusBar.tsx # Bottom bar with status/tokens +├── RalphPanel.tsx # Ralph loop progress display +└── HelpOverlay.tsx # Keyboard shortcuts modal +``` + +**Key Hook:** `useSessionManager.ts` handles: +- API polling for sessions/screens +- Screen attach/detach flow +- Keyboard input routing +- State synchronization with web server + +**TUI ↔ Web Server:** The TUI is a client to the web server. It doesn't manage sessions directly - it uses `/api/*` endpoints and attaches to screens via GNU screen commands. + ## Buffer Limits Long-running sessions are supported with automatic trimming: @@ -413,8 +460,8 @@ Long-running sessions are supported with automatic trimming: | POST | `/api/sessions/:id/respawn/stop` | Stop respawn controller | | POST | `/api/sessions/:id/respawn/enable` | Enable respawn with config + optional timer | | PUT | `/api/sessions/:id/respawn/config` | Update config on running respawn | -| POST | `/api/sessions/:id/inner-config` | Configure Ralph Wiggum loop settings | -| GET | `/api/sessions/:id/inner-state` | Get Ralph loop state + todos | +| POST | `/api/sessions/:id/ralph-config` | Configure Ralph / Todo Tracker settings | +| GET | `/api/sessions/:id/ralph-state` | Get Ralph loop state + todos | | POST | `/api/sessions/:id/auto-compact` | Configure auto-compact threshold | | POST | `/api/sessions/:id/auto-clear` | Configure auto-clear threshold | | POST | `/api/quick-start` | Create case + start session. Body: `{mode?: 'claude'\|'shell'}` | @@ -478,7 +525,7 @@ Long-running sessions are supported with automatic trimming: | File | Purpose | |------|---------| | `~/.claudeman/state.json` | Sessions, tasks, config | -| `~/.claudeman/state-inner.json` | Inner loop/todo state (separate to reduce writes) | +| `~/.claudeman/state-inner.json` | Ralph loop/todo state per session (separate to reduce writes) | | `~/.claudeman/screens.json` | Screen session metadata | Cases created in `~/claudeman-cases/` by default. @@ -540,6 +587,6 @@ Extended documentation is available in the `docs/` directory: /ralph-loop:help # Show help and usage ``` -**Claudeman Implementation**: The `InnerLoopTracker` class (`src/inner-loop-tracker.ts`) detects Ralph patterns in Claude output and tracks loop state, todos, and completion phrases. It auto-enables when Ralph-related patterns are detected. +**Claudeman Implementation**: The `RalphTracker` class (`src/ralph-tracker.ts`) detects Ralph patterns in Claude output and tracks loop state, todos, and completion phrases. It auto-enables when Ralph-related patterns are detected. See [`docs/ralph-wiggum-guide.md`](docs/ralph-wiggum-guide.md) for full documentation on best practices, prompt templates, and troubleshooting. diff --git a/README.md b/README.md index ad77470e..c3f0e160 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Node.js 18+ TypeScript 5.5 Fastify - Tests + Tests

--- diff --git a/src/ralph-config.ts b/src/ralph-config.ts new file mode 100644 index 00000000..5f480519 --- /dev/null +++ b/src/ralph-config.ts @@ -0,0 +1,153 @@ +/** + * @fileoverview Ralph Wiggum configuration parser + * + * Parses the official Ralph Wiggum plugin state file (.claude/ralph-loop.local.md) + * and extracts configuration for session tracking. + * + * @module ralph-config + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +// Pattern to extract completion phrase from CLAUDE.md +// Matches PHRASE with optional whitespace +// Allows: letters, numbers, underscores, hyphens (e.g., TESTS-PASS, ALL_DONE) +const CLAUDE_MD_PROMISE_PATTERN = /\s*([A-Z0-9_-]+)\s*<\/promise>/gi; + +// Pattern to parse YAML frontmatter from ralph-loop.local.md +// Extracts key: value pairs from between --- markers +const YAML_FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---/; +const YAML_LINE_PATTERN = /^([a-z-]+):\s*"?([^"\n]+)"?\s*$/gm; + +/** + * Ralph Loop configuration from .claude/ralph-loop.local.md + */ +export interface RalphLoopConfig { + enabled: boolean; + iteration: number; + maxIterations: number | null; + completionPromise: string | null; +} + +/** + * Parse Ralph Wiggum loop config from .claude/ralph-loop.local.md + * + * The official Ralph Wiggum plugin stores state in this file with YAML frontmatter: + * ```yaml + * --- + * enabled: true + * iteration: 5 + * max-iterations: 50 + * completion-promise: "COMPLETE" + * --- + * # Original Prompt + * ... + * ``` + * + * @param workingDir - Working directory to search in + * @returns Parsed config or null if file doesn't exist or is invalid + */ +export function parseRalphLoopConfig(workingDir: string): RalphLoopConfig | null { + const configPath = join(workingDir, '.claude', 'ralph-loop.local.md'); + + try { + if (!existsSync(configPath)) return null; + const content = readFileSync(configPath, 'utf-8'); + + return parseRalphLoopConfigFromContent(content); + } catch (err) { + console.error(`[ralph-config] Error reading ralph-loop.local.md: ${err}`); + return null; + } +} + +/** + * Parse Ralph Wiggum loop config from file content. + * Exported for testing. + * + * @param content - File content to parse + * @returns Parsed config or null if content is invalid + */ +export function parseRalphLoopConfigFromContent(content: string): RalphLoopConfig | null { + // Extract YAML frontmatter + const frontmatterMatch = content.match(YAML_FRONTMATTER_PATTERN); + if (!frontmatterMatch) return null; + + const yaml = frontmatterMatch[1]; + const config: RalphLoopConfig = { + enabled: false, + iteration: 0, + maxIterations: null, + completionPromise: null, + }; + + // Parse each YAML line + let match; + YAML_LINE_PATTERN.lastIndex = 0; + while ((match = YAML_LINE_PATTERN.exec(yaml)) !== null) { + const key = match[1].toLowerCase(); + const value = match[2].trim(); + + switch (key) { + case 'enabled': + config.enabled = value === 'true'; + break; + case 'iteration': + config.iteration = parseInt(value, 10) || 0; + break; + case 'max-iterations': + config.maxIterations = parseInt(value, 10) || null; + break; + case 'completion-promise': + config.completionPromise = value.toUpperCase(); + break; + } + } + + return config; +} + +/** + * Extract completion phrase from CLAUDE.md content. + * Looks for PHRASE pattern. + * + * Handles multiple variations: + * - Raw text: PHRASE + * - In backticks: `PHRASE` + * - With whitespace: PHRASE + * - Multiple occurrences: returns the first one + * + * @param claudeMdPath - Path to CLAUDE.md file + * @returns The completion phrase (uppercase), or null if not found + */ +export function extractCompletionPhrase(claudeMdPath: string): string | null { + try { + if (!existsSync(claudeMdPath)) return null; + const content = readFileSync(claudeMdPath, 'utf-8'); + + return extractCompletionPhraseFromContent(content); + } catch (err) { + console.error(`[ralph-config] Error reading CLAUDE.md: ${err}`); + return null; + } +} + +/** + * Extract completion phrase from CLAUDE.md content string. + * Exported for testing. + * + * @param content - CLAUDE.md content to parse + * @returns The completion phrase (uppercase), or null if not found + */ +export function extractCompletionPhraseFromContent(content: string): string | null { + // Reset regex state (global flag) + CLAUDE_MD_PROMISE_PATTERN.lastIndex = 0; + + // Find all matches and return the first one + const match = CLAUDE_MD_PROMISE_PATTERN.exec(content); + if (match && match[1]) { + return match[1].trim().toUpperCase(); + } + return null; +} diff --git a/src/inner-loop-tracker.ts b/src/ralph-tracker.ts similarity index 96% rename from src/inner-loop-tracker.ts rename to src/ralph-tracker.ts index cb3c0219..93243d62 100644 --- a/src/inner-loop-tracker.ts +++ b/src/ralph-tracker.ts @@ -1,5 +1,5 @@ /** - * @fileoverview Inner Loop Tracker for Ralph Wiggum detection + * @fileoverview Ralph Tracker - Detects Ralph Wiggum loops, todos, and completion phrases * * This module parses terminal output from Claude Code sessions to detect: * - Ralph Wiggum loop state (active, completion phrase, iteration count) @@ -10,15 +10,15 @@ * patterns are detected in the output stream, reducing overhead for * sessions not using autonomous loops. * - * @module inner-loop-tracker + * @module ralph-tracker */ import { EventEmitter } from 'node:events'; import { - InnerLoopState, - InnerTodoItem, - InnerTodoStatus, - createInitialInnerLoopState, + RalphTrackerState, + RalphTodoItem, + RalphTodoStatus, + createInitialRalphTrackerState, } from './types.js'; // ========== Configuration Constants ========== @@ -56,6 +56,12 @@ const EVENT_DEBOUNCE_MS = 50; * Matches completion phrase tags: `PHRASE` * Used to detect when Claude signals task completion. * Capture group 1: The completion phrase text + * + * Supports any characters between tags including: + * - Uppercase letters: COMPLETE, DONE + * - Numbers: TASK_123 + * - Underscores: ALL_TASKS_DONE + * - Hyphens: TESTS-PASS, TIME-COMPLETE */ const PROMISE_PATTERN = /([^<]+)<\/promise>/; @@ -194,17 +200,17 @@ const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g; // ========== Event Types ========== /** - * Events emitted by InnerLoopTracker + * Events emitted by RalphTracker * @event loopUpdate - Fired when loop state changes (active, iteration, completion phrase) * @event todoUpdate - Fired when todo list changes (items added, status changed) * @event completionDetected - Fired when completion phrase is detected (task complete) * @event enabled - Fired when tracker auto-enables due to Ralph pattern detection */ -export interface InnerLoopTrackerEvents { +export interface RalphTrackerEvents { /** Emitted when loop state changes */ - loopUpdate: (state: InnerLoopState) => void; + loopUpdate: (state: RalphTrackerState) => void; /** Emitted when todo list is modified */ - todoUpdate: (todos: InnerTodoItem[]) => void; + todoUpdate: (todos: RalphTodoItem[]) => void; /** Emitted when completion phrase detected (loop finished) */ completionDetected: (phrase: string) => void; /** Emitted when tracker auto-enables from disabled state */ @@ -212,7 +218,7 @@ export interface InnerLoopTrackerEvents { } /** - * InnerLoopTracker - Parses terminal output to detect Ralph Wiggum loops and todos + * RalphTracker - Parses terminal output to detect Ralph Wiggum loops and todos * * This class monitors Claude Code session output to detect: * 1. **Ralph Wiggum loop state** - Active loops, completion phrases, iteration counts @@ -242,19 +248,19 @@ export interface InnerLoopTrackerEvents { * @extends EventEmitter * @example * ```typescript - * const tracker = new InnerLoopTracker(); + * const tracker = new RalphTracker(); * tracker.on('completionDetected', (phrase) => { * console.log('Loop completed with phrase:', phrase); * }); * tracker.processTerminalData(ptyOutput); * ``` */ -export class InnerLoopTracker extends EventEmitter { +export class RalphTracker extends EventEmitter { /** Current state of the detected loop */ - private _loopState: InnerLoopState; + private _loopState: RalphTrackerState; /** Map of todo items by ID for O(1) lookup */ - private _todos: Map = new Map(); + private _todos: Map = new Map(); /** Buffer for incomplete lines from terminal data */ private _lineBuffer: string = ''; @@ -284,12 +290,12 @@ export class InnerLoopTracker extends EventEmitter { private _autoEnableDisabled: boolean = false; /** - * Creates a new InnerLoopTracker instance. + * Creates a new RalphTracker instance. * Starts in disabled state until Ralph patterns are detected. */ constructor() { super(); - this._loopState = createInitialInnerLoopState(); + this._loopState = createInitialRalphTrackerState(); } /** @@ -373,7 +379,7 @@ export class InnerLoopTracker extends EventEmitter { this.clearDebounceTimers(); const wasEnabled = this._loopState.enabled; - this._loopState = createInitialInnerLoopState(); + this._loopState = createInitialRalphTrackerState(); this._loopState.enabled = wasEnabled; // Keep enabled status this._todos.clear(); this._completionPhraseCount.clear(); @@ -394,7 +400,7 @@ export class InnerLoopTracker extends EventEmitter { // Clear debounce timers this.clearDebounceTimers(); - this._loopState = createInitialInnerLoopState(); + this._loopState = createInitialRalphTrackerState(); this._todos.clear(); this._completionPhraseCount.clear(); this._lineBuffer = ''; @@ -489,7 +495,7 @@ export class InnerLoopTracker extends EventEmitter { * Get a copy of the current loop state. * @returns Shallow copy of loop state (safe to modify) */ - get loopState(): InnerLoopState { + get loopState(): RalphTrackerState { return { ...this._loopState }; } @@ -497,7 +503,7 @@ export class InnerLoopTracker extends EventEmitter { * Get all tracked todo items as an array. * @returns Array of todo items (copy, safe to modify) */ - get todos(): InnerTodoItem[] { + get todos(): RalphTodoItem[] { return Array.from(this._todos.values()); } @@ -1032,7 +1038,7 @@ export class InnerLoopTracker extends EventEmitter { while ((match = TODO_CHECKBOX_PATTERN.exec(line)) !== null) { const checked = match[1].toLowerCase() === 'x'; const content = match[2].trim(); - const status: InnerTodoStatus = checked ? 'completed' : 'pending'; + const status: RalphTodoStatus = checked ? 'completed' : 'pending'; this.upsertTodo(content, status); updated = true; } @@ -1057,7 +1063,7 @@ export class InnerLoopTracker extends EventEmitter { TODO_STATUS_PATTERN.lastIndex = 0; while ((match = TODO_STATUS_PATTERN.exec(line)) !== null) { const content = match[1].trim(); - const status = match[2] as InnerTodoStatus; + const status = match[2] as RalphTodoStatus; this.upsertTodo(content, status); updated = true; } @@ -1099,9 +1105,9 @@ export class InnerLoopTracker extends EventEmitter { * - Pending: `☐`, `○`, and anything else (default) * * @param icon - Single character icon - * @returns Corresponding InnerTodoStatus + * @returns Corresponding RalphTodoStatus */ - private iconToStatus(icon: string): InnerTodoStatus { + private iconToStatus(icon: string): RalphTodoStatus { switch (icon) { case '✓': case '✅': @@ -1134,7 +1140,7 @@ export class InnerLoopTracker extends EventEmitter { * @param content - Raw todo content text * @param status - Status to set */ - private upsertTodo(content: string, status: InnerTodoStatus): void { + private upsertTodo(content: string, status: RalphTodoStatus): void { // Skip empty or whitespace-only content if (!content || !content.trim()) return; @@ -1225,8 +1231,8 @@ export class InnerLoopTracker extends EventEmitter { * Used for LRU eviction when at MAX_TODO_ITEMS limit. * @returns Oldest todo item, or undefined if map is empty */ - private findOldestTodo(): InnerTodoItem | undefined { - let oldest: InnerTodoItem | undefined; + private findOldestTodo(): RalphTodoItem | undefined { + let oldest: RalphTodoItem | undefined; for (const todo of this._todos.values()) { if (!oldest || todo.detectedAt < oldest.detectedAt) { oldest = todo; @@ -1334,7 +1340,7 @@ export class InnerLoopTracker extends EventEmitter { * @fires todoUpdate - With empty array */ clear(): void { - this._loopState = createInitialInnerLoopState(); // This sets enabled: false + this._loopState = createInitialRalphTrackerState(); // This sets enabled: false this._todos.clear(); this._lineBuffer = ''; this._completionPhraseCount.clear(); @@ -1389,7 +1395,7 @@ export class InnerLoopTracker extends EventEmitter { * @param loopState - Persisted loop state object * @param todos - Persisted todo items array */ - restoreState(loopState: InnerLoopState, todos: InnerTodoItem[]): void { + restoreState(loopState: RalphTrackerState, todos: RalphTodoItem[]): void { // Ensure enabled flag exists (backwards compatibility) this._loopState = { ...loopState, diff --git a/src/screen-manager.ts b/src/screen-manager.ts index 292473dd..218e3287 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -588,11 +588,11 @@ export class ScreenManager extends EventEmitter { } } - // Update inner loop (Ralph Wiggum) enabled state - updateInnerLoopEnabled(sessionId: string, enabled: boolean): void { + // Update Ralph enabled state + updateRalphEnabled(sessionId: string, enabled: boolean): void { const screen = this.screens.get(sessionId); if (screen) { - screen.innerLoopEnabled = enabled; + screen.ralphEnabled = enabled; this.saveScreens(); } } diff --git a/src/session.ts b/src/session.ts index 72d07ab6..177687a6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -18,13 +18,13 @@ import { EventEmitter } from 'node:events'; import { v4 as uuidv4 } from 'uuid'; import * as pty from 'node-pty'; -import { SessionState, SessionStatus, SessionConfig, ScreenSession, InnerLoopState, InnerTodoItem } from './types.js'; +import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js'; import { TaskTracker, type BackgroundTask } from './task-tracker.js'; -import { InnerLoopTracker } from './inner-loop-tracker.js'; +import { RalphTracker } from './ralph-tracker.js'; import { ScreenManager } from './screen-manager.js'; export type { BackgroundTask } from './task-tracker.js'; -export type { InnerLoopState, InnerTodoItem } from './types.js'; +export type { RalphTrackerState, RalphTodoItem } from './types.js'; export { withTimeout }; // ============================================================================ @@ -219,12 +219,12 @@ export interface SessionEvents { autoClear: (data: { tokens: number; threshold: number }) => void; /** Auto-compact triggered due to token threshold */ autoCompact: (data: { tokens: number; threshold: number; prompt?: string }) => void; - /** Inner loop (Ralph Wiggum) state changed */ - innerLoopUpdate: (state: InnerLoopState) => void; - /** Inner loop todo list updated */ - innerTodoUpdate: (todos: InnerTodoItem[]) => void; - /** Inner loop completion phrase detected */ - innerCompletionDetected: (phrase: string) => void; + /** Ralph loop state changed */ + ralphLoopUpdate: (state: RalphTrackerState) => void; + /** Ralph todo list updated */ + ralphTodoUpdate: (todos: RalphTodoItem[]) => void; + /** Ralph completion phrase detected */ + ralphCompletionDetected: (phrase: string) => void; } /** @@ -320,8 +320,8 @@ export class Session extends EventEmitter { private _screenSession: ScreenSession | null = null; private _useScreen: boolean = false; - // Inner loop tracking (Ralph Wiggum loops and todo lists inside Claude Code) - private _innerLoopTracker: InnerLoopTracker; + // Ralph tracking (Ralph Wiggum loops and todo lists inside Claude Code) + private _ralphTracker: RalphTracker; // Store handler references for cleanup (prevents memory leaks) private _taskTrackerHandlers: { @@ -331,9 +331,9 @@ export class Session extends EventEmitter { taskFailed: (task: BackgroundTask, error: string) => void; } | null = null; - private _innerLoopHandlers: { - loopUpdate: (state: InnerLoopState) => void; - todoUpdate: (todos: InnerTodoItem[]) => void; + private _ralphHandlers: { + loopUpdate: (state: RalphTrackerState) => void; + todoUpdate: (todos: RalphTodoItem[]) => void; completionDetected: (phrase: string) => void; } | null = null; @@ -369,16 +369,16 @@ export class Session extends EventEmitter { this._taskTracker.on('taskCompleted', this._taskTrackerHandlers.taskCompleted); this._taskTracker.on('taskFailed', this._taskTrackerHandlers.taskFailed); - // Initialize inner loop tracker and forward events (store handlers for cleanup) - this._innerLoopTracker = new InnerLoopTracker(); - this._innerLoopHandlers = { - loopUpdate: (state) => this.emit('innerLoopUpdate', state), - todoUpdate: (todos) => this.emit('innerTodoUpdate', todos), - completionDetected: (phrase) => this.emit('innerCompletionDetected', phrase), + // Initialize Ralph tracker and forward events (store handlers for cleanup) + this._ralphTracker = new RalphTracker(); + this._ralphHandlers = { + loopUpdate: (state) => this.emit('ralphLoopUpdate', state), + todoUpdate: (todos) => this.emit('ralphTodoUpdate', todos), + completionDetected: (phrase) => this.emit('ralphCompletionDetected', phrase), }; - this._innerLoopTracker.on('loopUpdate', this._innerLoopHandlers.loopUpdate); - this._innerLoopTracker.on('todoUpdate', this._innerLoopHandlers.todoUpdate); - this._innerLoopTracker.on('completionDetected', this._innerLoopHandlers.completionDetected); + this._ralphTracker.on('loopUpdate', this._ralphHandlers.loopUpdate); + this._ralphTracker.on('todoUpdate', this._ralphHandlers.todoUpdate); + this._ralphTracker.on('completionDetected', this._ralphHandlers.completionDetected); } get status(): SessionStatus { @@ -449,21 +449,21 @@ export class Session extends EventEmitter { return this._taskTracker.getStats(); } - // Inner loop tracking getters - get innerLoopTracker(): InnerLoopTracker { - return this._innerLoopTracker; + // Ralph tracking getters + get ralphTracker(): RalphTracker { + return this._ralphTracker; } - get innerLoopState(): InnerLoopState { - return this._innerLoopTracker.loopState; + get ralphLoopState(): RalphTrackerState { + return this._ralphTracker.loopState; } - get innerTodos(): InnerTodoItem[] { - return this._innerLoopTracker.todos; + get ralphTodos(): RalphTodoItem[] { + return this._ralphTracker.todos; } - get innerTodoStats(): { total: number; pending: number; inProgress: number; completed: number } { - return this._innerLoopTracker.getTodoStats(); + get ralphTodoStats(): { total: number; pending: number; inProgress: number; completed: number } { + return this._ralphTracker.getTodoStats(); } // Token tracking getters and setters @@ -582,10 +582,10 @@ export class Session extends EventEmitter { enabled: this._autoClearEnabled, threshold: this._autoClearThreshold, }, - // Inner loop tracking state - innerLoop: this._innerLoopTracker.loopState, - innerTodos: this._innerLoopTracker.todos, - innerTodoStats: this._innerLoopTracker.getTodoStats(), + // Ralph tracking state + ralphLoop: this._ralphTracker.loopState, + ralphTodos: this._ralphTracker.todos, + ralphTodoStats: this._ralphTracker.getTodoStats(), }; } @@ -726,8 +726,8 @@ export class Session extends EventEmitter { this.emit('terminal', data); this.emit('output', data); - // Forward to inner loop tracker to detect Ralph loops and todos - this._innerLoopTracker.processTerminalData(data); + // Forward to Ralph tracker to detect Ralph loops and todos + this._ralphTracker.processTerminalData(data); // Parse token count from status line (e.g., "123.4k tokens" or "5234 tokens") this.parseTokensFromStatusLine(data); @@ -1344,7 +1344,7 @@ export class Session extends EventEmitter { } /** - * Remove event listeners from TaskTracker and InnerLoopTracker. + * Remove event listeners from TaskTracker and RalphTracker. * Prevents memory leaks by ensuring handlers don't persist after session stop. */ private cleanupTrackerListeners(): void { @@ -1357,12 +1357,12 @@ export class Session extends EventEmitter { this._taskTrackerHandlers = null; } - // Remove InnerLoopTracker handlers - if (this._innerLoopHandlers) { - this._innerLoopTracker.off('loopUpdate', this._innerLoopHandlers.loopUpdate); - this._innerLoopTracker.off('todoUpdate', this._innerLoopHandlers.todoUpdate); - this._innerLoopTracker.off('completionDetected', this._innerLoopHandlers.completionDetected); - this._innerLoopHandlers = null; + // Remove RalphTracker handlers + if (this._ralphHandlers) { + this._ralphTracker.off('loopUpdate', this._ralphHandlers.loopUpdate); + this._ralphTracker.off('todoUpdate', this._ralphHandlers.todoUpdate); + this._ralphTracker.off('completionDetected', this._ralphHandlers.completionDetected); + this._ralphHandlers = null; } } @@ -1527,6 +1527,6 @@ export class Session extends EventEmitter { this._errorBuffer = ''; this._messages = []; this._taskTracker.clear(); - this._innerLoopTracker.clear(); + this._ralphTracker.clear(); } } diff --git a/src/state-store.ts b/src/state-store.ts index 6128c752..b132af7f 100644 --- a/src/state-store.ts +++ b/src/state-store.ts @@ -8,7 +8,7 @@ * - `state.json`: Main app state (sessions, tasks, config) * - `state-inner.json`: Inner loop state (todos, Ralph loop state per session) * - * The separation reduces write frequency since inner loop state changes rapidly + * The separation reduces write frequency since Ralph state changes rapidly * during Ralph Wiggum loops. * * @module state-store @@ -17,7 +17,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; import { homedir } from 'node:os'; import { dirname, join } from 'node:path'; -import { AppState, createInitialState, InnerSessionState, createInitialInnerSessionState } from './types.js'; +import { AppState, createInitialState, RalphSessionState, createInitialRalphSessionState } from './types.js'; /** Debounce delay for batching state writes (ms) */ const SAVE_DEBOUNCE_MS = 500; @@ -50,17 +50,17 @@ export class StateStore { private dirty: boolean = false; // Inner state storage (separate from main state to reduce write frequency) - private innerStates: Map = new Map(); - private innerStatePath: string; - private innerStateSaveTimeout: NodeJS.Timeout | null = null; - private innerStateDirty: boolean = false; + private ralphStates: Map = new Map(); + private ralphStatePath: string; + private ralphStateSaveTimeout: NodeJS.Timeout | null = null; + private ralphStateDirty: boolean = false; constructor(filePath?: string) { this.filePath = filePath || join(homedir(), '.claudeman', 'state.json'); - this.innerStatePath = this.filePath.replace('.json', '-inner.json'); + this.ralphStatePath = this.filePath.replace('.json', '-inner.json'); this.state = this.load(); this.state.config.stateFilePath = this.filePath; - this.loadInnerStates(); + this.loadRalphStates(); } private ensureDir(): void { @@ -203,20 +203,20 @@ export class StateStore { reset(): void { this.state = createInitialState(); this.state.config.stateFilePath = this.filePath; - this.innerStates.clear(); + this.ralphStates.clear(); this.saveNow(); // Immediate save for reset operations - this.saveInnerStatesNow(); + this.saveRalphStatesNow(); } // ========== Inner State Methods (Ralph Loop tracking) ========== - private loadInnerStates(): void { + private loadRalphStates(): void { try { - if (existsSync(this.innerStatePath)) { - const data = readFileSync(this.innerStatePath, 'utf-8'); - const parsed = JSON.parse(data) as Record; + if (existsSync(this.ralphStatePath)) { + const data = readFileSync(this.ralphStatePath, 'utf-8'); + const parsed = JSON.parse(data) as Record; for (const [sessionId, state] of Object.entries(parsed)) { - this.innerStates.set(sessionId, state); + this.ralphStates.set(sessionId, state); } } } catch (err) { @@ -225,43 +225,43 @@ export class StateStore { } // Debounced save for inner states - private saveInnerStates(): void { - this.innerStateDirty = true; - if (this.innerStateSaveTimeout) { + private saveRalphStates(): void { + this.ralphStateDirty = true; + if (this.ralphStateSaveTimeout) { return; // Already scheduled } - this.innerStateSaveTimeout = setTimeout(() => { - this.saveInnerStatesNow(); + this.ralphStateSaveTimeout = setTimeout(() => { + this.saveRalphStatesNow(); }, SAVE_DEBOUNCE_MS); } // Immediate save for inner states - private saveInnerStatesNow(): void { - if (this.innerStateSaveTimeout) { - clearTimeout(this.innerStateSaveTimeout); - this.innerStateSaveTimeout = null; + private saveRalphStatesNow(): void { + if (this.ralphStateSaveTimeout) { + clearTimeout(this.ralphStateSaveTimeout); + this.ralphStateSaveTimeout = null; } - if (!this.innerStateDirty) { + if (!this.ralphStateDirty) { return; } - this.innerStateDirty = false; + this.ralphStateDirty = false; this.ensureDir(); - const data: Record = {}; - for (const [sessionId, state] of this.innerStates) { + const data: Record = {}; + for (const [sessionId, state] of this.ralphStates) { data[sessionId] = state; } - writeFileSync(this.innerStatePath, JSON.stringify(data, null, 2), 'utf-8'); + writeFileSync(this.ralphStatePath, JSON.stringify(data, null, 2), 'utf-8'); } /** Returns inner state for a session, or null if not found. */ - getInnerState(sessionId: string): InnerSessionState | null { - return this.innerStates.get(sessionId) || null; + getRalphState(sessionId: string): RalphSessionState | null { + return this.ralphStates.get(sessionId) || null; } /** Sets inner state for a session and triggers a debounced save. */ - setInnerState(sessionId: string, state: InnerSessionState): void { - this.innerStates.set(sessionId, state); - this.saveInnerStates(); + setRalphState(sessionId: string, state: RalphSessionState): void { + this.ralphStates.set(sessionId, state); + this.saveRalphStates(); } /** @@ -269,34 +269,34 @@ export class StateStore { * Creates initial state if none exists. * @returns The updated inner state. */ - updateInnerState(sessionId: string, updates: Partial): InnerSessionState { - let state = this.innerStates.get(sessionId); + updateRalphState(sessionId: string, updates: Partial): RalphSessionState { + let state = this.ralphStates.get(sessionId); if (!state) { - state = createInitialInnerSessionState(sessionId); + state = createInitialRalphSessionState(sessionId); } state = { ...state, ...updates, lastUpdated: Date.now() }; - this.innerStates.set(sessionId, state); - this.saveInnerStates(); + this.ralphStates.set(sessionId, state); + this.saveRalphStates(); return state; } /** Removes inner state for a session and triggers a debounced save. */ - removeInnerState(sessionId: string): void { - if (this.innerStates.has(sessionId)) { - this.innerStates.delete(sessionId); - this.saveInnerStates(); + removeRalphState(sessionId: string): void { + if (this.ralphStates.has(sessionId)) { + this.ralphStates.delete(sessionId); + this.saveRalphStates(); } } /** Returns a copy of all inner states as a Map. */ - getAllInnerStates(): Map { - return new Map(this.innerStates); + getAllRalphStates(): Map { + return new Map(this.ralphStates); } /** Flushes all pending saves (main and inner state). Call before shutdown. */ flushAll(): void { this.saveNow(); - this.saveInnerStatesNow(); + this.saveRalphStatesNow(); } } diff --git a/src/tui/App.tsx b/src/tui/App.tsx index bd181e68..d7cf313d 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -9,7 +9,7 @@ * - **Main view**: Active session management with: * - TabBar: Session tabs with switching * - TerminalView: Live terminal output display - * - RalphPanel: Inner loop tracking (conditional) + * - RalphPanel: Ralph loop tracking (conditional) * - StatusBar: Session info and keyboard hints * * Keyboard shortcuts are handled globally via Ink's useInput hook. @@ -116,8 +116,8 @@ export function App(): React.ReactElement { prevSession, sendInput, terminalOutput, - innerLoopState, - innerTodos, + ralphLoopState, + ralphTodos, respawnStatus, cases, lastUsedCase, @@ -427,7 +427,7 @@ export function App(): React.ReactElement { } // Check if Ralph panel should be visible (enabled and has data) - const showRalphPanel = innerLoopState?.enabled && (innerLoopState.active || innerTodos.length > 0); + const showRalphPanel = ralphLoopState?.enabled && (ralphLoopState.active || ralphTodos.length > 0); // Adjust terminal height if Ralph panel is shown const adjustedTerminalHeight = showRalphPanel ? terminalHeight - 6 : terminalHeight; @@ -449,8 +449,8 @@ export function App(): React.ReactElement { {showRalphPanel && ( )} diff --git a/src/tui/components/RalphPanel.tsx b/src/tui/components/RalphPanel.tsx index 49338eb2..c7aac66e 100644 --- a/src/tui/components/RalphPanel.tsx +++ b/src/tui/components/RalphPanel.tsx @@ -11,17 +11,17 @@ * - Progress through todos with visual indicators * - Cycle count and elapsed time tracking * - * @see {@link file://./../../inner-loop-tracker.ts} for detection logic + * @see {@link file://./../../ralph-tracker.ts} for detection logic * @see {@link file://./../../../docs/ralph-wiggum-guide.md} for full documentation */ import React from 'react'; import { Box, Text } from 'ink'; -import type { InnerLoopState, InnerTodoItem } from '../../types.js'; +import type { RalphTrackerState, RalphTodoItem } from '../../types.js'; interface RalphPanelProps { - loopState: InnerLoopState | null; - todos: InnerTodoItem[]; + loopState: RalphTrackerState | null; + todos: RalphTodoItem[]; visible?: boolean; } @@ -70,7 +70,7 @@ function formatElapsed(hours: number | null): string { * Only renders when loop is enabled and has relevant data to show. * * @param props - Component props - * @param props.loopState - Current loop state from InnerLoopTracker + * @param props.loopState - Current loop state from RalphTracker * @param props.todos - Array of todo items being tracked * @param props.visible - Whether the panel should be visible (default: true) * @returns The panel element or null if hidden/disabled diff --git a/src/tui/hooks/useSessionManager.ts b/src/tui/hooks/useSessionManager.ts index 1965d36d..84dd5bdf 100644 --- a/src/tui/hooks/useSessionManager.ts +++ b/src/tui/hooks/useSessionManager.ts @@ -7,7 +7,7 @@ * This hook provides a complete interface for managing Claude sessions: * - Session discovery from ~/.claudeman/screens.json * - Terminal output polling via GNU screen hardcopy - * - Inner loop state tracking for Ralph Wiggum loops + * - Ralph state tracking for Ralph Wiggum loops * - Respawn status monitoring via the Claudeman API * * @example @@ -29,12 +29,12 @@ import { homedir } from 'os'; import { join } from 'path'; import { execSync, exec } from 'child_process'; import { promisify } from 'util'; -import type { ScreenSession, InnerLoopState, InnerTodoItem, InnerSessionState } from '../../types.js'; +import type { ScreenSession, RalphTrackerState, RalphTodoItem, RalphSessionState } from '../../types.js'; const execAsync = promisify(exec); const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json'); -const INNER_STATE_FILE = join(homedir(), '.claudeman', 'state-inner.json'); +const RALPH_STATE_FILE = join(homedir(), '.claudeman', 'state-inner.json'); const SETTINGS_FILE = join(homedir(), '.claudeman', 'settings.json'); const OUTPUT_POLL_INTERVAL = 300; // Poll terminal output every 300ms (faster refresh) @@ -149,8 +149,8 @@ interface SessionManagerState { activeSessionId: string | null; activeSession: ScreenSession | null; terminalOutput: string; - innerLoopState: InnerLoopState | null; - innerTodos: InnerTodoItem[]; + ralphLoopState: RalphTrackerState | null; + ralphTodos: RalphTodoItem[]; respawnStatus: RespawnStatus | null; cases: CaseInfo[]; lastUsedCase: string | null; @@ -237,18 +237,18 @@ function loadSessions(): ScreenSession[] { * * @description * Reads ~/.claudeman/state-inner.json which contains per-session - * tracking of inner loops, todos, and completion phrases. + * tracking of Ralph loops, todos, and completion phrases. * * @param sessionId - The UUID of the session to load state for - * @returns The inner session state or null if not found + * @returns The Ralph session state or null if not found */ -function loadInnerState(sessionId: string): InnerSessionState | null { +function loadRalphState(sessionId: string): RalphSessionState | null { try { - if (!existsSync(INNER_STATE_FILE)) { + if (!existsSync(RALPH_STATE_FILE)) { return null; } - const data = readFileSync(INNER_STATE_FILE, 'utf-8'); - const allStates = JSON.parse(data) as Record; + const data = readFileSync(RALPH_STATE_FILE, 'utf-8'); + const allStates = JSON.parse(data) as Record; return allStates[sessionId] || null; } catch { return null; @@ -284,7 +284,7 @@ function loadLastUsedCase(): string | null { * Provides complete session lifecycle management: * - Automatic session discovery and status monitoring * - Terminal output polling (500ms interval) - * - Inner loop state tracking (500ms interval) + * - Ralph state tracking (500ms interval) * - Respawn status polling via API (2000ms interval) * - Session CRUD operations via Claudeman API * @@ -311,8 +311,8 @@ export function useSessionManager(): SessionManagerState { const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); const [terminalOutput, setTerminalOutput] = useState(''); - const [innerLoopState, setInnerLoopState] = useState(null); - const [innerTodos, setInnerTodos] = useState([]); + const [ralphLoopState, setRalphTrackerState] = useState(null); + const [ralphTodos, setInnerTodos] = useState([]); const [respawnStatus, setRespawnStatus] = useState(null); const [cases, setCases] = useState([]); const [lastUsedCase, setLastUsedCase] = useState(loadLastUsedCase); @@ -346,30 +346,30 @@ export function useSessionManager(): SessionManagerState { // Get active session const activeSession = sessions.find((s) => s.sessionId === activeSessionId) || null; - // Poll inner state for active session + // Poll Ralph state for active session useEffect(() => { if (!activeSessionId) { - setInnerLoopState(null); + setRalphTrackerState(null); setInnerTodos([]); return; } - const pollInnerState = () => { - const state = loadInnerState(activeSessionId); + const pollRalphState = () => { + const state = loadRalphState(activeSessionId); if (state) { - setInnerLoopState(state.loop); + setRalphTrackerState(state.loop); setInnerTodos(state.todos); } else { - setInnerLoopState(null); + setRalphTrackerState(null); setInnerTodos([]); } }; // Initial poll - pollInnerState(); + pollRalphState(); // Set up polling interval (same as output polling) - const intervalId = setInterval(pollInnerState, OUTPUT_POLL_INTERVAL); + const intervalId = setInterval(pollRalphState, OUTPUT_POLL_INTERVAL); return () => { clearInterval(intervalId); @@ -809,8 +809,8 @@ export function useSessionManager(): SessionManagerState { activeSessionId, activeSession, terminalOutput, - innerLoopState, - innerTodos, + ralphLoopState, + ralphTodos, respawnStatus, cases, lastUsedCase, diff --git a/src/types.ts b/src/types.ts index b6b13dba..0d766c3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -475,8 +475,8 @@ export interface ScreenSession { name?: string; /** Persisted respawn controller configuration (restored on server restart) */ respawnConfig?: PersistedRespawnConfig; - /** Whether inner loop (Ralph Wiggum) tracking is enabled */ - innerLoopEnabled?: boolean; + /** Whether Ralph / Todo tracking is enabled */ + ralphEnabled?: boolean; } /** @@ -533,12 +533,12 @@ export const DEFAULT_CONFIG: AppConfig = { */ /** Status of a detected todo item */ -export type InnerTodoStatus = 'pending' | 'in_progress' | 'completed'; +export type RalphTodoStatus = 'pending' | 'in_progress' | 'completed'; /** - * State of an inner loop (Ralph Wiggum loop inside Claude Code) + * State of per-session Ralph / Todo tracking (detected from Claude output) */ -export interface InnerLoopState { +export interface RalphTrackerState { /** Whether the tracker is actively monitoring (disabled by default) */ enabled: boolean; /** Whether a loop is currently active */ @@ -560,27 +560,27 @@ export interface InnerLoopState { /** * A detected todo item from Claude Code output */ -export interface InnerTodoItem { +export interface RalphTodoItem { /** Unique identifier based on content hash */ id: string; /** Todo item text content */ content: string; /** Current status */ - status: InnerTodoStatus; + status: RalphTodoStatus; /** Timestamp when detected */ detectedAt: number; } /** - * Complete inner state for a session + * Complete Ralph/todo state for a session */ -export interface InnerSessionState { +export interface RalphSessionState { /** Session this state belongs to */ sessionId: string; /** Loop tracking state */ - loop: InnerLoopState; + loop: RalphTrackerState; /** Detected todo items */ - todos: InnerTodoItem[]; + todos: RalphTodoItem[]; /** Timestamp of last update */ lastUpdated: number; } @@ -588,15 +588,15 @@ export interface InnerSessionState { /** * Map of session ID to inner state */ -export interface InnerStateRecord { - [sessionId: string]: InnerSessionState; +export interface RalphStateRecord { + [sessionId: string]: RalphSessionState; } /** - * Creates initial inner loop state - * @returns Fresh inner loop state with defaults + * Creates initial Ralph tracker state + * @returns Fresh Ralph tracker state with defaults */ -export function createInitialInnerLoopState(): InnerLoopState { +export function createInitialRalphTrackerState(): RalphTrackerState { return { enabled: false, // Disabled by default, auto-enables when Ralph patterns detected active: false, @@ -610,14 +610,14 @@ export function createInitialInnerLoopState(): InnerLoopState { } /** - * Creates initial inner session state + * Creates initial Ralph session state * @param sessionId Session ID this state belongs to - * @returns Fresh inner session state + * @returns Fresh Ralph session state */ -export function createInitialInnerSessionState(sessionId: string): InnerSessionState { +export function createInitialRalphSessionState(sessionId: string): RalphSessionState { return { sessionId, - loop: createInitialInnerLoopState(), + loop: createInitialRalphTrackerState(), todos: [], lastUpdated: Date.now(), }; diff --git a/src/web/public/app.js b/src/web/public/app.js index 94d026d7..80c3a22d 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -19,9 +19,9 @@ class ClaudemanApp { this.pendingCloseSessionId = null; // Session pending close confirmation this.screenSessions = []; // Screen sessions for process monitor - // Inner loop/todo state per session - this.innerStates = new Map(); // Map - this.innerStatePanelCollapsed = true; // Default to collapsed + // Ralph loop/todo state per session + this.ralphStates = new Map(); // Map + this.ralphStatePanelCollapsed = true; // Default to collapsed // Terminal write batching this.pendingWrites = ''; @@ -29,7 +29,7 @@ class ClaudemanApp { // Render debouncing this.renderSessionTabsTimeout = null; - this.renderInnerStatePanelTimeout = null; + this.renderRalphStatePanelTimeout = null; this.renderTaskPanelTimeout = null; this.renderScreenSessionsTimeout = null; @@ -302,14 +302,14 @@ class ClaudemanApp { const data = JSON.parse(e.data); this.sessions.delete(data.id); this.terminalBuffers.delete(data.id); - this.innerStates.delete(data.id); // Clean up inner state for this session + this.ralphStates.delete(data.id); // Clean up ralph state for this session if (this.activeSessionId === data.id) { this.activeSessionId = null; this.terminal.clear(); this.showWelcome(); } this.renderSessionTabs(); - this.renderInnerStatePanel(); // Update inner panel after session deleted + this.renderRalphStatePanel(); // Update ralph panel after session deleted }); this.eventSource.addEventListener('session:terminal', (e) => { @@ -543,18 +543,18 @@ class ClaudemanApp { } }); - // Inner loop/todo events - this.eventSource.addEventListener('session:innerLoopUpdate', (e) => { + // Ralph loop/todo events + this.eventSource.addEventListener('session:ralphLoopUpdate', (e) => { const data = JSON.parse(e.data); - this.updateInnerState(data.sessionId, { loop: data.state }); + this.updateRalphState(data.sessionId, { loop: data.state }); }); - this.eventSource.addEventListener('session:innerTodoUpdate', (e) => { + this.eventSource.addEventListener('session:ralphTodoUpdate', (e) => { const data = JSON.parse(e.data); - this.updateInnerState(data.sessionId, { todos: data.todos }); + this.updateRalphState(data.sessionId, { todos: data.todos }); }); - this.eventSource.addEventListener('session:innerCompletionDetected', (e) => { + this.eventSource.addEventListener('session:ralphCompletionDetected', (e) => { const data = JSON.parse(e.data); // Prevent duplicate notifications for the same completion const completionKey = `${data.sessionId}:${data.phrase}`; @@ -568,11 +568,11 @@ class ClaudemanApp { // Clear after 30 seconds to allow re-notification if loop restarts setTimeout(() => this._shownCompletions?.delete(completionKey), 30000); - // Update inner state to mark loop as inactive - const existing = this.innerStates.get(data.sessionId) || {}; + // Update ralph state to mark loop as inactive + const existing = this.ralphStates.get(data.sessionId) || {}; if (existing.loop) { existing.loop.active = false; - this.updateInnerState(data.sessionId, existing); + this.updateRalphState(data.sessionId, existing); } }); } @@ -586,14 +586,14 @@ class ClaudemanApp { handleInit(data) { this.sessions.clear(); - this.innerStates.clear(); + this.ralphStates.clear(); data.sessions.forEach(s => { this.sessions.set(s.id, s); - // Load inner state from session data - if (s.innerLoop || s.innerTodos) { - this.innerStates.set(s.id, { - loop: s.innerLoop || null, - todos: s.innerTodos || [] + // Load ralph state from session data + if (s.ralphLoop || s.ralphTodos) { + this.ralphStates.set(s.id, { + loop: s.ralphLoop || null, + todos: s.ralphTodos || [] }); } }); @@ -807,15 +807,15 @@ class ClaudemanApp { this.renderTaskPanel(); } - // Update inner state panel for this session + // Update ralph state panel for this session const session = this.sessions.get(sessionId); - if (session && (session.innerLoop || session.innerTodos)) { - this.updateInnerState(sessionId, { - loop: session.innerLoop, - todos: session.innerTodos + if (session && (session.ralphLoop || session.ralphTodos)) { + this.updateRalphState(sessionId, { + loop: session.ralphLoop, + todos: session.ralphTodos }); } - this.renderInnerStatePanel(); + this.renderRalphStatePanel(); this.terminal.focus(); } catch (err) { @@ -828,7 +828,7 @@ class ClaudemanApp { await fetch(`/api/sessions/${sessionId}?killScreen=${killScreen}`, { method: 'DELETE' }); this.sessions.delete(sessionId); this.terminalBuffers.delete(sessionId); - this.innerStates.delete(sessionId); + this.ralphStates.delete(sessionId); if (this.activeSessionId === sessionId) { this.activeSessionId = null; @@ -839,7 +839,7 @@ class ClaudemanApp { } else { this.terminal.clear(); this.showWelcome(); - this.renderInnerStatePanel(); // Clear inner panel when no sessions + this.renderRalphStatePanel(); // Clear ralph panel when no sessions } } @@ -1088,7 +1088,7 @@ class ClaudemanApp { // Apply global Ralph tracker setting // If enabled: enable the tracker // If disabled: disable auto-enable to prevent pattern-based activation - await fetch(`/api/sessions/${createData.session.id}/inner-config`, { + await fetch(`/api/sessions/${createData.session.id}/ralph-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -1518,13 +1518,13 @@ class ClaudemanApp { this.selectDurationPreset(''); // Populate Ralph Wiggum form with current session values - const innerState = this.innerStates.get(sessionId); + const ralphState = this.ralphStates.get(sessionId); this.populateRalphForm({ - enabled: innerState?.loop?.enabled ?? session.innerConfig?.enabled ?? false, - completionPhrase: innerState?.loop?.completionPhrase || session.innerLoop?.completionPhrase || '', - maxIterations: innerState?.loop?.maxIterations || session.innerLoop?.maxIterations || 0, - maxTodos: session.innerConfig?.maxTodos || 50, - todoExpirationMinutes: session.innerConfig?.todoExpirationMinutes || 60 + enabled: ralphState?.loop?.enabled ?? session.ralphConfig?.enabled ?? false, + completionPhrase: ralphState?.loop?.completionPhrase || session.ralphLoop?.completionPhrase || '', + maxIterations: ralphState?.loop?.maxIterations || session.ralphLoop?.maxIterations || 0, + maxTodos: session.ralphConfig?.maxTodos || 50, + todoExpirationMinutes: session.ralphConfig?.todoExpirationMinutes || 60 }); document.getElementById('sessionOptionsModal').classList.add('active'); @@ -1728,7 +1728,7 @@ class ClaudemanApp { const config = this.getRalphConfig(); try { - const res = await fetch(`/api/sessions/${this.editingSessionId}/inner-config`, { + const res = await fetch(`/api/sessions/${this.editingSessionId}/ralph-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) @@ -2177,24 +2177,24 @@ class ClaudemanApp { // ========== Enhanced Ralph Wiggum Loop Panel ========== - updateInnerState(sessionId, updates) { - const existing = this.innerStates.get(sessionId) || { loop: null, todos: [] }; + updateRalphState(sessionId, updates) { + const existing = this.ralphStates.get(sessionId) || { loop: null, todos: [] }; const updated = { ...existing, ...updates }; - this.innerStates.set(sessionId, updated); + this.ralphStates.set(sessionId, updated); // Re-render if this is the active session if (sessionId === this.activeSessionId) { - this.renderInnerStatePanel(); + this.renderRalphStatePanel(); } } - toggleInnerStatePanel() { + toggleRalphStatePanel() { // Preserve xterm scroll position to prevent jump when panel height changes const xtermViewport = this.terminal?.element?.querySelector('.xterm-viewport'); const scrollTop = xtermViewport?.scrollTop; - this.innerStatePanelCollapsed = !this.innerStatePanelCollapsed; - this.renderInnerStatePanel(); + this.ralphStatePanelCollapsed = !this.ralphStatePanelCollapsed; + this.renderRalphStatePanel(); // Restore scroll position and refit terminal after layout change requestAnimationFrame(() => { @@ -2213,19 +2213,19 @@ class ClaudemanApp { if (!this.activeSessionId) return; // Disable tracker via API - await fetch(`/api/sessions/${this.activeSessionId}/inner-config`, { + await fetch(`/api/sessions/${this.activeSessionId}/ralph-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: false }) }); // Clear local state and hide panel - this.innerStates.delete(this.activeSessionId); - this.renderInnerStatePanel(); + this.ralphStates.delete(this.activeSessionId); + this.renderRalphStatePanel(); } toggleRalphDetach() { - const panel = this.$('innerStatePanel'); + const panel = this.$('ralphStatePanel'); const detachBtn = this.$('ralphDetachBtn'); if (!panel) return; @@ -2245,7 +2245,7 @@ class ClaudemanApp { // Detach as floating window panel.classList.add('detached'); // Expand when detaching for better visibility - this.innerStatePanelCollapsed = false; + this.ralphStatePanelCollapsed = false; panel.classList.remove('collapsed'); if (detachBtn) { detachBtn.innerHTML = '⊞'; // Attach icon (squared plus - dock back) @@ -2254,11 +2254,11 @@ class ClaudemanApp { // Setup drag functionality this.setupRalphDrag(); } - this.renderInnerStatePanel(); + this.renderRalphStatePanel(); } setupRalphDrag() { - const panel = this.$('innerStatePanel'); + const panel = this.$('ralphStatePanel'); const header = this.$('ralphSummary'); if (!panel || !header) return; @@ -2313,23 +2313,23 @@ class ClaudemanApp { header.addEventListener('mousedown', onMouseDown); } - renderInnerStatePanel() { + renderRalphStatePanel() { // Debounce renders at 50ms to prevent excessive DOM updates - if (this.renderInnerStatePanelTimeout) { - clearTimeout(this.renderInnerStatePanelTimeout); + if (this.renderRalphStatePanelTimeout) { + clearTimeout(this.renderRalphStatePanelTimeout); } - this.renderInnerStatePanelTimeout = setTimeout(() => { - this._renderInnerStatePanelImmediate(); + this.renderRalphStatePanelTimeout = setTimeout(() => { + this._renderRalphStatePanelImmediate(); }, 50); } - _renderInnerStatePanelImmediate() { - const panel = this.$('innerStatePanel'); + _renderRalphStatePanelImmediate() { + const panel = this.$('ralphStatePanel'); const toggle = this.$('ralphToggle'); if (!panel) return; - const state = this.innerStates.get(this.activeSessionId); + const state = this.ralphStates.get(this.activeSessionId); // Check if there's anything to show // Only show panel if tracker is enabled OR there's active state to display @@ -2360,7 +2360,7 @@ class ClaudemanApp { this.updateRalphStats(state?.loop, completed, total); // Handle collapsed/expanded state - if (this.innerStatePanelCollapsed) { + if (this.ralphStatePanelCollapsed) { panel.classList.add('collapsed'); if (toggle) toggle.innerHTML = '▼'; // Down arrow when collapsed (click to expand) } else { @@ -2510,8 +2510,8 @@ class ClaudemanApp { if (!grid) return; if (todos.length === 0) { - if (grid.children.length !== 1 || !grid.querySelector('.inner-state-empty')) { - grid.innerHTML = '
No tasks detected
'; + if (grid.children.length !== 1 || !grid.querySelector('.ralph-state-empty')) { + grid.innerHTML = '
No tasks detected
'; } return; } diff --git a/src/web/public/index.html b/src/web/public/index.html index 24e4d16a..5ca337fc 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -72,11 +72,11 @@ - -