mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
feat: persist full session state to state.json for CLI visibility
The web server now persists all per-session settings (name, mode, auto-compact, auto-clear, respawn config, ralph state, tokens, cost) to state.json so the CLI status/session commands can display them. Also removes unused pendingStepConfirm property from respawn controller. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+33
-4
@@ -92,7 +92,25 @@ sessionCmd
|
|||||||
if (stoppedSessions.length > 0) {
|
if (stoppedSessions.length > 0) {
|
||||||
console.log(chalk.bold('\nStopped Sessions:'));
|
console.log(chalk.bold('\nStopped Sessions:'));
|
||||||
for (const session of stoppedSessions) {
|
for (const session of stoppedSessions) {
|
||||||
console.log(` ${chalk.gray(session.id.slice(0, 8))} ${chalk.gray('stopped')} ${session.workingDir}`);
|
const name = session.name ? ` (${session.name})` : '';
|
||||||
|
console.log(` ${chalk.gray(session.id.slice(0, 8))} ${chalk.gray('stopped')}${name} ${session.workingDir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show active sessions from state (when web server manages them)
|
||||||
|
const activeSessions = Object.values(stored).filter((s) => s.status !== 'stopped');
|
||||||
|
if (sessions.length === 0 && activeSessions.length > 0) {
|
||||||
|
console.log(chalk.bold('\nActive Sessions (from web server):'));
|
||||||
|
for (const session of activeSessions) {
|
||||||
|
const status = session.status === 'idle'
|
||||||
|
? chalk.green('idle')
|
||||||
|
: session.status === 'busy'
|
||||||
|
? chalk.yellow('busy')
|
||||||
|
: chalk.red(session.status);
|
||||||
|
const name = session.name ? ` (${session.name})` : '';
|
||||||
|
const mode = session.mode === 'shell' ? chalk.gray(' [shell]') : '';
|
||||||
|
const cost = session.totalCost ? chalk.gray(` $${session.totalCost.toFixed(4)}`) : '';
|
||||||
|
console.log(` ${chalk.cyan(session.id.slice(0, 8))} ${status}${name}${mode}${cost} ${session.workingDir}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -371,16 +389,27 @@ program
|
|||||||
const loop = getRalphLoop();
|
const loop = getRalphLoop();
|
||||||
|
|
||||||
const sessions = manager.getAllSessions();
|
const sessions = manager.getAllSessions();
|
||||||
|
const stored = manager.getStoredSessions();
|
||||||
|
const storedValues = Object.values(stored);
|
||||||
const taskCounts = queue.getCount();
|
const taskCounts = queue.getCount();
|
||||||
const loopStatus = loop.status;
|
const loopStatus = loop.status;
|
||||||
|
|
||||||
|
// Use live sessions if available, otherwise fall back to stored state
|
||||||
|
const activeCount = sessions.length || storedValues.filter((s) => s.status !== 'stopped').length;
|
||||||
|
const idleCount = sessions.length
|
||||||
|
? sessions.filter((s) => s.isIdle()).length
|
||||||
|
: storedValues.filter((s) => s.status === 'idle').length;
|
||||||
|
const busyCount = sessions.length
|
||||||
|
? sessions.filter((s) => s.isBusy()).length
|
||||||
|
: storedValues.filter((s) => s.status === 'busy').length;
|
||||||
|
|
||||||
console.log(chalk.bold('\nClaudeman Status'));
|
console.log(chalk.bold('\nClaudeman Status'));
|
||||||
console.log('─'.repeat(40));
|
console.log('─'.repeat(40));
|
||||||
|
|
||||||
console.log(chalk.bold('\nSessions:'));
|
console.log(chalk.bold('\nSessions:'));
|
||||||
console.log(` Active: ${sessions.length}`);
|
console.log(` Active: ${activeCount}`);
|
||||||
console.log(` Idle: ${sessions.filter((s) => s.isIdle()).length}`);
|
console.log(` Idle: ${idleCount}`);
|
||||||
console.log(` Busy: ${sessions.filter((s) => s.isBusy()).length}`);
|
console.log(` Busy: ${busyCount}`);
|
||||||
|
|
||||||
console.log(chalk.bold('\nTasks:'));
|
console.log(chalk.bold('\nTasks:'));
|
||||||
console.log(` Total: ${taskCounts.total}`);
|
console.log(` Total: ${taskCounts.total}`);
|
||||||
|
|||||||
@@ -413,9 +413,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
/** Timer for step completion confirmation (waits for silence after completion) */
|
/** Timer for step completion confirmation (waits for silence after completion) */
|
||||||
private stepConfirmTimer: NodeJS.Timeout | null = null;
|
private stepConfirmTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
/** Which step is pending confirmation */
|
|
||||||
private pendingStepConfirm: 'update' | 'init' | 'kickstart' | null = null;
|
|
||||||
|
|
||||||
/** Fallback timeout for /clear step (ms) - sends /init without waiting for prompt */
|
/** Fallback timeout for /clear step (ms) - sends /init without waiting for prompt */
|
||||||
private static readonly CLEAR_FALLBACK_TIMEOUT_MS = 10000;
|
private static readonly CLEAR_FALLBACK_TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
@@ -996,7 +993,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
if (this.stepConfirmTimer) {
|
if (this.stepConfirmTimer) {
|
||||||
clearTimeout(this.stepConfirmTimer);
|
clearTimeout(this.stepConfirmTimer);
|
||||||
this.stepConfirmTimer = null;
|
this.stepConfirmTimer = null;
|
||||||
this.pendingStepConfirm = null;
|
|
||||||
}
|
}
|
||||||
if (this.noOutputTimer) {
|
if (this.noOutputTimer) {
|
||||||
clearTimeout(this.noOutputTimer);
|
clearTimeout(this.noOutputTimer);
|
||||||
@@ -1112,7 +1109,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
clearTimeout(this.stepConfirmTimer);
|
clearTimeout(this.stepConfirmTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pendingStepConfirm = step;
|
|
||||||
this.log(`Step '${step}' completion detected, waiting ${this.config.completionConfirmMs}ms for silence...`);
|
this.log(`Step '${step}' completion detected, waiting ${this.config.completionConfirmMs}ms for silence...`);
|
||||||
|
|
||||||
this.stepConfirmTimer = setTimeout(() => {
|
this.stepConfirmTimer = setTimeout(() => {
|
||||||
@@ -1121,7 +1117,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
if (msSinceOutput >= this.config.completionConfirmMs) {
|
if (msSinceOutput >= this.config.completionConfirmMs) {
|
||||||
this.log(`Step '${step}' confirmed: ${msSinceOutput}ms silence`);
|
this.log(`Step '${step}' confirmed: ${msSinceOutput}ms silence`);
|
||||||
this.stepConfirmTimer = null;
|
this.stepConfirmTimer = null;
|
||||||
this.pendingStepConfirm = null;
|
|
||||||
|
|
||||||
// Proceed with the step completion
|
// Proceed with the step completion
|
||||||
switch (step) {
|
switch (step) {
|
||||||
@@ -1152,7 +1148,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
this.stepConfirmTimer = null;
|
this.stepConfirmTimer = null;
|
||||||
this.log(`Step confirmation cancelled (working detected)`);
|
this.log(`Step confirmation cancelled (working detected)`);
|
||||||
}
|
}
|
||||||
this.pendingStepConfirm = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -547,6 +547,18 @@ export class Session extends EventEmitter {
|
|||||||
currentTaskId: this._currentTaskId,
|
currentTaskId: this._currentTaskId,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
lastActivityAt: this._lastActivityAt,
|
lastActivityAt: this._lastActivityAt,
|
||||||
|
name: this._name,
|
||||||
|
mode: this.mode,
|
||||||
|
autoClearEnabled: this._autoClearEnabled,
|
||||||
|
autoClearThreshold: this._autoClearThreshold,
|
||||||
|
autoCompactEnabled: this._autoCompactEnabled,
|
||||||
|
autoCompactThreshold: this._autoCompactThreshold,
|
||||||
|
autoCompactPrompt: this._autoCompactPrompt,
|
||||||
|
totalCost: this._totalCost,
|
||||||
|
inputTokens: this._totalInputTokens,
|
||||||
|
outputTokens: this._totalOutputTokens,
|
||||||
|
ralphEnabled: this._ralphTracker.enabled,
|
||||||
|
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,32 @@ export interface SessionState {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
/** Timestamp of last activity */
|
/** Timestamp of last activity */
|
||||||
lastActivityAt: number;
|
lastActivityAt: number;
|
||||||
|
/** Session display name */
|
||||||
|
name?: string;
|
||||||
|
/** Session mode: 'claude' or 'shell' */
|
||||||
|
mode?: 'claude' | 'shell';
|
||||||
|
/** Auto-clear enabled */
|
||||||
|
autoClearEnabled?: boolean;
|
||||||
|
/** Auto-clear token threshold */
|
||||||
|
autoClearThreshold?: number;
|
||||||
|
/** Auto-compact enabled */
|
||||||
|
autoCompactEnabled?: boolean;
|
||||||
|
/** Auto-compact token threshold */
|
||||||
|
autoCompactThreshold?: number;
|
||||||
|
/** Auto-compact prompt */
|
||||||
|
autoCompactPrompt?: string;
|
||||||
|
/** Total cost in USD */
|
||||||
|
totalCost?: number;
|
||||||
|
/** Input tokens used */
|
||||||
|
inputTokens?: number;
|
||||||
|
/** Output tokens used */
|
||||||
|
outputTokens?: number;
|
||||||
|
/** Respawn controller config (if enabled) */
|
||||||
|
respawnConfig?: RespawnConfig & { durationMinutes?: number };
|
||||||
|
/** Ralph / Todo tracker enabled */
|
||||||
|
ralphEnabled?: boolean;
|
||||||
|
/** Ralph completion phrase (if set) */
|
||||||
|
ralphCompletionPhrase?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Task Types ==========
|
// ========== Task Types ==========
|
||||||
@@ -171,6 +197,10 @@ export interface RespawnConfig {
|
|||||||
sendInit: boolean;
|
sendInit: boolean;
|
||||||
/** Optional prompt to send if /init doesn't trigger work */
|
/** Optional prompt to send if /init doesn't trigger work */
|
||||||
kickstartPrompt?: string;
|
kickstartPrompt?: string;
|
||||||
|
/** Time to wait after completion message before confirming idle (ms) */
|
||||||
|
completionConfirmMs?: number;
|
||||||
|
/** Fallback timeout when no output received at all (ms) */
|
||||||
|
noOutputTimeoutMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+50
-3
@@ -225,6 +225,7 @@ export class WebServer extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.setupSessionListeners(session);
|
this.setupSessionListeners(session);
|
||||||
|
|
||||||
this.broadcast('session:created', session.toDetailedState());
|
this.broadcast('session:created', session.toDetailedState());
|
||||||
@@ -244,6 +245,7 @@ export class WebServer extends EventEmitter {
|
|||||||
session.name = body.name || '';
|
session.name = body.name || '';
|
||||||
// Also update the screen name if this session has a screen
|
// Also update the screen name if this session has a screen
|
||||||
this.screenManager.updateScreenName(id, session.name);
|
this.screenManager.updateScreenName(id, session.name);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.broadcast('session:updated', session.toDetailedState());
|
this.broadcast('session:updated', session.toDetailedState());
|
||||||
return { success: true, name: session.name };
|
return { success: true, name: session.name };
|
||||||
});
|
});
|
||||||
@@ -392,7 +394,8 @@ export class WebServer extends EventEmitter {
|
|||||||
todoExpirationMinutes: todoExpirationMinutes || 60
|
todoExpirationMinutes: todoExpirationMinutes || 60
|
||||||
};
|
};
|
||||||
|
|
||||||
// Broadcast the update
|
// Persist and broadcast the update
|
||||||
|
this.persistSessionState(session);
|
||||||
this.broadcast('session:ralphLoopUpdate', {
|
this.broadcast('session:ralphLoopUpdate', {
|
||||||
sessionId: id,
|
sessionId: id,
|
||||||
state: session.ralphLoopState
|
state: session.ralphLoopState
|
||||||
@@ -600,8 +603,9 @@ export class WebServer extends EventEmitter {
|
|||||||
|
|
||||||
controller.start();
|
controller.start();
|
||||||
|
|
||||||
// Persist respawn config to screen session
|
// Persist respawn config to screen session and state.json
|
||||||
this.saveRespawnConfig(id, controller.getConfig());
|
this.saveRespawnConfig(id, controller.getConfig());
|
||||||
|
this.persistSessionState(session);
|
||||||
|
|
||||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||||
|
|
||||||
@@ -622,6 +626,12 @@ export class WebServer extends EventEmitter {
|
|||||||
// Clear persisted respawn config
|
// Clear persisted respawn config
|
||||||
this.screenManager.clearRespawnConfig(id);
|
this.screenManager.clearRespawnConfig(id);
|
||||||
|
|
||||||
|
// Update state.json (respawnConfig removed)
|
||||||
|
const session = this.sessions.get(id);
|
||||||
|
if (session) {
|
||||||
|
this.persistSessionState(session);
|
||||||
|
}
|
||||||
|
|
||||||
this.broadcast('respawn:stopped', { sessionId: id });
|
this.broadcast('respawn:stopped', { sessionId: id });
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -641,6 +651,10 @@ export class WebServer extends EventEmitter {
|
|||||||
|
|
||||||
// Persist updated config
|
// Persist updated config
|
||||||
this.saveRespawnConfig(id, controller.getConfig());
|
this.saveRespawnConfig(id, controller.getConfig());
|
||||||
|
const session = this.sessions.get(id);
|
||||||
|
if (session) {
|
||||||
|
this.persistSessionState(session);
|
||||||
|
}
|
||||||
|
|
||||||
this.broadcast('respawn:configUpdated', { sessionId: id, config: controller.getConfig() });
|
this.broadcast('respawn:configUpdated', { sessionId: id, config: controller.getConfig() });
|
||||||
|
|
||||||
@@ -681,6 +695,9 @@ export class WebServer extends EventEmitter {
|
|||||||
this.setupTimedRespawn(id, body.durationMinutes);
|
this.setupTimedRespawn(id, body.durationMinutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist full session state with respawn config
|
||||||
|
this.persistSessionState(session);
|
||||||
|
|
||||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -727,8 +744,9 @@ export class WebServer extends EventEmitter {
|
|||||||
this.setupTimedRespawn(id, body.durationMinutes);
|
this.setupTimedRespawn(id, body.durationMinutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist respawn config to screen session
|
// Persist respawn config to screen session and state.json
|
||||||
this.saveRespawnConfig(id, controller.getConfig(), body?.durationMinutes);
|
this.saveRespawnConfig(id, controller.getConfig(), body?.durationMinutes);
|
||||||
|
this.persistSessionState(session);
|
||||||
|
|
||||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||||
|
|
||||||
@@ -758,6 +776,7 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.setAutoClear(body.enabled, body.threshold);
|
session.setAutoClear(body.enabled, body.threshold);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.broadcast('session:updated', session.toDetailedState());
|
this.broadcast('session:updated', session.toDetailedState());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -790,6 +809,7 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.setAutoCompact(body.enabled, body.threshold, body.prompt);
|
session.setAutoCompact(body.enabled, body.threshold, body.prompt);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.broadcast('session:updated', session.toDetailedState());
|
this.broadcast('session:updated', session.toDetailedState());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -820,6 +840,7 @@ export class WebServer extends EventEmitter {
|
|||||||
|
|
||||||
const session = new Session({ workingDir: dir });
|
const session = new Session({ workingDir: dir });
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.setupSessionListeners(session);
|
this.setupSessionListeners(session);
|
||||||
|
|
||||||
this.broadcast('session:created', session.toDetailedState());
|
this.broadcast('session:created', session.toDetailedState());
|
||||||
@@ -1101,6 +1122,7 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.setupSessionListeners(session);
|
this.setupSessionListeners(session);
|
||||||
this.broadcast('session:created', session.toDetailedState());
|
this.broadcast('session:created', session.toDetailedState());
|
||||||
|
|
||||||
@@ -1217,6 +1239,21 @@ export class WebServer extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Persists full session state including respawn config to state.json */
|
||||||
|
private persistSessionState(session: Session): void {
|
||||||
|
const state = session.toState();
|
||||||
|
const controller = this.respawnControllers.get(session.id);
|
||||||
|
if (controller) {
|
||||||
|
const config = controller.getConfig();
|
||||||
|
const timerInfo = this.respawnTimers.get(session.id);
|
||||||
|
const durationMinutes = timerInfo
|
||||||
|
? Math.round((timerInfo.endAt - timerInfo.startedAt) / 60000)
|
||||||
|
: undefined;
|
||||||
|
state.respawnConfig = { ...config, durationMinutes };
|
||||||
|
}
|
||||||
|
this.store.setSession(session.id, state);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to save respawn config to screen session for persistence
|
// Helper to save respawn config to screen session for persistence
|
||||||
private saveRespawnConfig(sessionId: string, config: RespawnConfig, durationMinutes?: number): void {
|
private saveRespawnConfig(sessionId: string, config: RespawnConfig, durationMinutes?: number): void {
|
||||||
const persistedConfig: PersistedRespawnConfig = {
|
const persistedConfig: PersistedRespawnConfig = {
|
||||||
@@ -1311,6 +1348,7 @@ export class WebServer extends EventEmitter {
|
|||||||
session.removeAllListeners();
|
session.removeAllListeners();
|
||||||
await session.stop(killScreen);
|
await session.stop(killScreen);
|
||||||
this.sessions.delete(sessionId);
|
this.sessions.delete(sessionId);
|
||||||
|
this.store.removeSession(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.broadcast('session:deleted', { id: sessionId });
|
this.broadcast('session:deleted', { id: sessionId });
|
||||||
@@ -1343,6 +1381,7 @@ export class WebServer extends EventEmitter {
|
|||||||
session.on('completion', (result, cost) => {
|
session.on('completion', (result, cost) => {
|
||||||
this.broadcast('session:completion', { id: session.id, result, cost });
|
this.broadcast('session:completion', { id: session.id, result, cost });
|
||||||
this.broadcast('session:updated', session.toDetailedState());
|
this.broadcast('session:updated', session.toDetailedState());
|
||||||
|
this.persistSessionState(session);
|
||||||
});
|
});
|
||||||
|
|
||||||
session.on('exit', (code) => {
|
session.on('exit', (code) => {
|
||||||
@@ -1350,6 +1389,7 @@ export class WebServer extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
this.broadcast('session:exit', { id: session.id, code });
|
this.broadcast('session:exit', { id: session.id, code });
|
||||||
this.broadcast('session:updated', session.toDetailedState());
|
this.broadcast('session:updated', session.toDetailedState());
|
||||||
|
this.persistSessionState(session);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Server] Error broadcasting session exit for ${session.id}:`, err);
|
console.error(`[Server] Error broadcasting session exit for ${session.id}:`, err);
|
||||||
}
|
}
|
||||||
@@ -1478,6 +1518,11 @@ export class WebServer extends EventEmitter {
|
|||||||
this.broadcast('respawn:stopped', { sessionId, reason: 'duration_expired' });
|
this.broadcast('respawn:stopped', { sessionId, reason: 'duration_expired' });
|
||||||
}
|
}
|
||||||
this.respawnTimers.delete(sessionId);
|
this.respawnTimers.delete(sessionId);
|
||||||
|
// Update persisted state (respawn no longer active)
|
||||||
|
const session = this.sessions.get(sessionId);
|
||||||
|
if (session) {
|
||||||
|
this.persistSessionState(session);
|
||||||
|
}
|
||||||
}, durationMinutes * 60 * 1000);
|
}, durationMinutes * 60 * 1000);
|
||||||
|
|
||||||
this.respawnTimers.set(sessionId, { timer, endAt, startedAt: now });
|
this.respawnTimers.set(sessionId, { timer, endAt, startedAt: now });
|
||||||
@@ -1557,6 +1602,7 @@ export class WebServer extends EventEmitter {
|
|||||||
// Create a session for this iteration
|
// Create a session for this iteration
|
||||||
session = new Session({ workingDir: run.workingDir });
|
session = new Session({ workingDir: run.workingDir });
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.setupSessionListeners(session);
|
this.setupSessionListeners(session);
|
||||||
run.sessionId = session.id;
|
run.sessionId = session.id;
|
||||||
|
|
||||||
@@ -1884,6 +1930,7 @@ export class WebServer extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
|
this.persistSessionState(session);
|
||||||
this.setupSessionListeners(session);
|
this.setupSessionListeners(session);
|
||||||
|
|
||||||
// Restore Ralph tracking state (Ralph Wiggum settings) if it was saved
|
// Restore Ralph tracking state (Ralph Wiggum settings) if it was saved
|
||||||
|
|||||||
Reference in New Issue
Block a user