From fc319d8987101d4e1c3ac16977a8661b755ece23 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 18:56:34 +0100 Subject: [PATCH] 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 --- src/web/public/app.js | 23 +++++++++++++ src/web/public/index.html | 17 ++++++++++ src/web/public/styles.css | 71 +++++++++++++++++++++++++++++++++++++++ src/web/server.ts | 13 ++++++- 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index c94201c7..c4d74218 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -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'); diff --git a/src/web/public/index.html b/src/web/public/index.html index b298148b..38700e4f 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -376,6 +376,15 @@
diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 012501b8..df29f7c2 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -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; diff --git a/src/web/server.ts b/src/web/server.ts index 5387ce14..84096c00 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -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,