feat(ui): add enable/disable toggle for Ralph Wiggum tracker

Add ability to enable/disable the Ralph tracker:
- Per-session toggle in Session Options > Ralph Wiggum tab
- Global default toggle in App Settings

Changes:
- Add "Enable Tracker" toggle to ralph-tab in session options modal
- Add "Enable Ralph Wiggum Tracker" toggle to app settings modal
- Update /api/sessions/:id/inner-config endpoint to handle `enabled` param
- Add CSS for toggle switch component
- New sessions inherit global setting from app settings
- Setting is saved in innerConfig and synced with backend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 18:56:34 +01:00
co-authored by Claude Opus 4.5
parent 6a414a2648
commit fc319d8987
4 changed files with 123 additions and 1 deletions
+23
View File
@@ -892,6 +892,9 @@ class ClaudemanApp {
}
}
// Get global Ralph tracker setting
const ralphEnabled = this.isRalphTrackerEnabledByDefault();
// Create multiple sessions with unique w-numbers
for (let i = 0; i < tabCount; i++) {
const sessionName = `w${startNumber + i}-${caseName}`;
@@ -905,6 +908,15 @@ class ClaudemanApp {
const createData = await createRes.json();
if (!createData.success) throw new Error(createData.error);
// Apply global Ralph tracker setting
if (ralphEnabled) {
await fetch(`/api/sessions/${createData.session.id}/inner-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: true })
});
}
// Start interactive mode
await fetch(`/api/sessions/${createData.session.id}/interactive`, {
method: 'POST'
@@ -1347,6 +1359,7 @@ class ClaudemanApp {
// Populate Ralph Wiggum form with current session values
const innerState = this.innerStates.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,
@@ -1529,6 +1542,7 @@ class ClaudemanApp {
getRalphConfig() {
return {
enabled: document.getElementById('modalRalphEnabled').checked,
completionPhrase: document.getElementById('modalRalphPhrase').value.trim(),
maxIterations: parseInt(document.getElementById('modalRalphMaxIterations').value) || 0,
maxTodos: parseInt(document.getElementById('modalRalphMaxTodos').value) || 50,
@@ -1537,6 +1551,7 @@ class ClaudemanApp {
}
populateRalphForm(config) {
document.getElementById('modalRalphEnabled').checked = config?.enabled ?? false;
document.getElementById('modalRalphPhrase').value = config?.completionPhrase || '';
document.getElementById('modalRalphMaxIterations').value = config?.maxIterations || 0;
document.getElementById('modalRalphMaxTodos').value = config?.maxTodos || 50;
@@ -1625,6 +1640,7 @@ class ClaudemanApp {
const settings = this.loadAppSettingsFromStorage();
document.getElementById('appSettingsClaudeMdPath').value = settings.defaultClaudeMdPath || '';
document.getElementById('appSettingsDefaultDir').value = settings.defaultWorkingDir || '';
document.getElementById('appSettingsRalphEnabled').checked = settings.ralphTrackerEnabled ?? false;
document.getElementById('appSettingsModal').classList.add('active');
}
@@ -1636,6 +1652,7 @@ class ClaudemanApp {
const settings = {
defaultClaudeMdPath: document.getElementById('appSettingsClaudeMdPath').value.trim(),
defaultWorkingDir: document.getElementById('appSettingsDefaultDir').value.trim(),
ralphTrackerEnabled: document.getElementById('appSettingsRalphEnabled').checked,
};
// Save to localStorage
@@ -1657,6 +1674,12 @@ class ClaudemanApp {
this.closeAppSettings();
}
// Get the global Ralph tracker enabled setting
isRalphTrackerEnabledByDefault() {
const settings = this.loadAppSettingsFromStorage();
return settings.ralphTrackerEnabled ?? false;
}
loadAppSettingsFromStorage() {
try {
const saved = localStorage.getItem('claudeman-app-settings');
+17
View File
@@ -376,6 +376,15 @@
<!-- Ralph Wiggum Tab Content -->
<div class="modal-tab-content hidden" id="ralph-tab">
<div class="form-row form-row-switch">
<label>Enable Tracker</label>
<label class="switch">
<input type="checkbox" id="modalRalphEnabled">
<span class="slider"></span>
</label>
<span class="form-hint">Enable Ralph Wiggum tracker for this session (auto-detects loops and todos)</span>
</div>
<div class="form-row">
<label>Completion Phrase</label>
<input type="text" id="modalRalphPhrase" placeholder="e.g., COMPLETE, DONE, FINISHED">
@@ -457,6 +466,14 @@
<input type="text" id="appSettingsDefaultDir" placeholder="~/projects">
<span class="form-hint">Default directory for new sessions.</span>
</div>
<div class="form-row form-row-switch">
<label>Enable Ralph Wiggum Tracker</label>
<label class="switch">
<input type="checkbox" id="appSettingsRalphEnabled">
<span class="slider"></span>
</label>
<span class="form-hint">Enable tracker by default for new sessions. When disabled, tracker auto-enables when Ralph patterns are detected.</span>
</div>
</div>
<div class="form-actions">
<button class="btn-toolbar" onclick="app.closeAppSettings()">Cancel</button>
+71
View File
@@ -1466,6 +1466,77 @@ body {
margin-top: 0.35rem;
}
/* Switch toggle for form rows */
.form-row-switch {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.form-row-switch > label:first-child {
margin-bottom: 0;
min-width: 120px;
}
.form-row-switch .form-hint {
flex-basis: 100%;
margin-top: 0.25rem;
}
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
flex-shrink: 0;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch .slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-input);
border: 1px solid var(--border);
transition: 0.2s;
border-radius: 20px;
}
.switch .slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: var(--text-muted);
transition: 0.2s;
border-radius: 50%;
}
.switch input:checked + .slider {
background-color: var(--accent);
border-color: var(--accent);
}
.switch input:checked + .slider:before {
transform: translateX(16px);
background-color: white;
}
.switch input:focus + .slider {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
.form-actions {
display: flex;
justify-content: flex-end;
+12 -1
View File
@@ -245,7 +245,8 @@ export class WebServer extends EventEmitter {
// Configure inner loop (Ralph Wiggum) settings
this.app.post('/api/sessions/:id/inner-config', async (req) => {
const { id } = req.params as { id: string };
const { completionPhrase, maxIterations, maxTodos, todoExpirationMinutes } = req.body as {
const { enabled, completionPhrase, maxIterations, maxTodos, todoExpirationMinutes } = req.body as {
enabled?: boolean;
completionPhrase?: string;
maxIterations?: number;
maxTodos?: number;
@@ -257,6 +258,15 @@ export class WebServer extends EventEmitter {
return { success: false, error: 'Session not found' };
}
// Enable/disable the tracker
if (enabled !== undefined) {
if (enabled) {
session.innerLoopTracker.enable();
} else {
session.innerLoopTracker.disable();
}
}
// Configure the inner loop tracker
if (completionPhrase !== undefined) {
// Start loop with completion phrase to set it up for watching
@@ -271,6 +281,7 @@ export class WebServer extends EventEmitter {
// Store additional config on session for reference
(session as any).innerConfig = {
enabled: enabled ?? session.innerLoopTracker.enabled,
completionPhrase: completionPhrase || '',
maxIterations: maxIterations || 0,
maxTodos: maxTodos || 50,