mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat: track Ralph Wiggum loops and todo lists in Claude sessions
Add InnerLoopTracker to detect and expose internal Claude Code state running inside claudeman sessions by parsing terminal output patterns. Detection patterns: - Completion phrases: <promise>PHRASE</promise> - Todo items: checkbox format, indicator icons, status parentheses - Loop status: cycle counts, elapsed time, start/completion New features: - Inner state panel UI (collapsible, below session tabs) - SSE events: innerLoopUpdate, innerTodoUpdate, innerCompletionDetected - API endpoint: GET /api/sessions/:id/inner-state - State persistence to ~/.claudeman/state-inner.json Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+93
-1
@@ -1,7 +1,7 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { AppState, createInitialState } from './types.js';
|
||||
import { AppState, createInitialState, InnerSessionState, createInitialInnerSessionState } from './types.js';
|
||||
|
||||
// Debounce delay for batching state writes (ms)
|
||||
const SAVE_DEBOUNCE_MS = 500;
|
||||
@@ -12,10 +12,18 @@ export class StateStore {
|
||||
private saveTimeout: NodeJS.Timeout | null = null;
|
||||
private dirty: boolean = false;
|
||||
|
||||
// Inner state storage (separate from main state to reduce write frequency)
|
||||
private innerStates: Map<string, InnerSessionState> = new Map();
|
||||
private innerStatePath: string;
|
||||
private innerStateSaveTimeout: NodeJS.Timeout | null = null;
|
||||
private innerStateDirty: boolean = false;
|
||||
|
||||
constructor(filePath?: string) {
|
||||
this.filePath = filePath || join(homedir(), '.claudeman', 'state.json');
|
||||
this.innerStatePath = this.filePath.replace('.json', '-inner.json');
|
||||
this.state = this.load();
|
||||
this.state.config.stateFilePath = this.filePath;
|
||||
this.loadInnerStates();
|
||||
}
|
||||
|
||||
private ensureDir(): void {
|
||||
@@ -138,7 +146,91 @@ export class StateStore {
|
||||
reset(): void {
|
||||
this.state = createInitialState();
|
||||
this.state.config.stateFilePath = this.filePath;
|
||||
this.innerStates.clear();
|
||||
this.saveNow(); // Immediate save for reset operations
|
||||
this.saveInnerStatesNow();
|
||||
}
|
||||
|
||||
// ========== Inner State Methods ==========
|
||||
|
||||
private loadInnerStates(): void {
|
||||
try {
|
||||
if (existsSync(this.innerStatePath)) {
|
||||
const data = readFileSync(this.innerStatePath, 'utf-8');
|
||||
const parsed = JSON.parse(data) as Record<string, InnerSessionState>;
|
||||
for (const [sessionId, state] of Object.entries(parsed)) {
|
||||
this.innerStates.set(sessionId, state);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load inner states:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced save for inner states
|
||||
private saveInnerStates(): void {
|
||||
this.innerStateDirty = true;
|
||||
if (this.innerStateSaveTimeout) {
|
||||
return; // Already scheduled
|
||||
}
|
||||
this.innerStateSaveTimeout = setTimeout(() => {
|
||||
this.saveInnerStatesNow();
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Immediate save for inner states
|
||||
private saveInnerStatesNow(): void {
|
||||
if (this.innerStateSaveTimeout) {
|
||||
clearTimeout(this.innerStateSaveTimeout);
|
||||
this.innerStateSaveTimeout = null;
|
||||
}
|
||||
if (!this.innerStateDirty) {
|
||||
return;
|
||||
}
|
||||
this.innerStateDirty = false;
|
||||
this.ensureDir();
|
||||
const data: Record<string, InnerSessionState> = {};
|
||||
for (const [sessionId, state] of this.innerStates) {
|
||||
data[sessionId] = state;
|
||||
}
|
||||
writeFileSync(this.innerStatePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
getInnerState(sessionId: string): InnerSessionState | null {
|
||||
return this.innerStates.get(sessionId) || null;
|
||||
}
|
||||
|
||||
setInnerState(sessionId: string, state: InnerSessionState): void {
|
||||
this.innerStates.set(sessionId, state);
|
||||
this.saveInnerStates();
|
||||
}
|
||||
|
||||
updateInnerState(sessionId: string, updates: Partial<InnerSessionState>): InnerSessionState {
|
||||
let state = this.innerStates.get(sessionId);
|
||||
if (!state) {
|
||||
state = createInitialInnerSessionState(sessionId);
|
||||
}
|
||||
state = { ...state, ...updates, lastUpdated: Date.now() };
|
||||
this.innerStates.set(sessionId, state);
|
||||
this.saveInnerStates();
|
||||
return state;
|
||||
}
|
||||
|
||||
removeInnerState(sessionId: string): void {
|
||||
if (this.innerStates.has(sessionId)) {
|
||||
this.innerStates.delete(sessionId);
|
||||
this.saveInnerStates();
|
||||
}
|
||||
}
|
||||
|
||||
getAllInnerStates(): Map<string, InnerSessionState> {
|
||||
return new Map(this.innerStates);
|
||||
}
|
||||
|
||||
// Flush all pending saves (call before shutdown)
|
||||
flushAll(): void {
|
||||
this.saveNow();
|
||||
this.saveInnerStatesNow();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user