From ce1d871b1ec8677d7ad7a9ada6cca2e733439d34 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 21 Jan 2026 04:19:34 +0100 Subject: [PATCH] feat(ui): add 20-tab support and Claude CLI settings - Increase max tab count from 10 to 20 - Fix header to properly expand for multiple tab rows (up to 4 rows) - Add scrollbar for tabs area when many tabs exist - Add Claude CLI startup mode setting (skip permissions/normal/allowed tools) - Reorganize settings modal into sections (CLI, Paths, Features) - Fix case select dropdown to fit content width - Create optimization todos document for future work Co-Authored-By: Claude Opus 4.5 --- .claude/optimization-todos.md | 149 ++++++++++++++++++++++++++++++++++ src/web/public/app.js | 84 ++++++++++--------- src/web/public/index.html | 24 +++++- src/web/public/styles.css | 40 ++++++--- 4 files changed, 242 insertions(+), 55 deletions(-) create mode 100644 .claude/optimization-todos.md diff --git a/.claude/optimization-todos.md b/.claude/optimization-todos.md new file mode 100644 index 00000000..72c70446 --- /dev/null +++ b/.claude/optimization-todos.md @@ -0,0 +1,149 @@ +# Claudeman Code Optimization TODO List + +> Generated: 2026-01-21 +> Purpose: Comprehensive optimization roadmap for future sessions + +## Overview + +This document contains prioritized optimization tasks identified through codebase analysis. +Each item has specific file:line references and expected impact estimates. + +--- + +## HIGH PRIORITY OPTIMIZATIONS + +### 1. String Concatenation in Hot Paths +- **Files**: `src/session.ts:460, 677-686`, `src/respawn-controller.ts:469, 826`, `src/inner-loop-tracker.ts:408` +- **Issue**: String `+=` in terminal buffer accumulation creates GC pressure +- **Fix**: Use array-based accumulation with periodic joins +- **Impact**: 10-20% faster terminal streaming + +### 2. Regex Pattern lastIndex Reset +- **Files**: `src/inner-loop-tracker.ts:464-479`, `src/screen-manager.ts:264` +- **Issue**: Global regex without lastIndex reset causes skip-every-other-match bug +- **Fix**: Always reset `pattern.lastIndex = 0` before use +- **Impact**: Correct pattern matching, eliminate intermittent bugs + +### 3. Unbounded Buffer Growth +- **Files**: `src/respawn-controller.ts:268-269`, `src/inner-loop-tracker.ts:257-258` +- **Issue**: Buffers lack proper size limits or have loose limits +- **Fix**: Add hard limits with Ring Buffer pattern +- **Impact**: Bounded memory, prevent OOM in long sessions + +### 4. Event Listener Management +- **Files**: `src/respawn-controller.ts:446-455`, `src/session.ts:481-501`, `src/web/server.ts:987-1084` +- **Issue**: Listeners created repeatedly without cleanup tracking +- **Fix**: Use debounce/throttle utilities, consolidate timeout management +- **Impact**: Fewer memory leaks, reduced GC sweeps + +### 5. Task Lookup Optimization +- **Files**: `src/task-tracker.ts:214-220, 231-234` +- **Issue**: O(n) sequential iteration for task lookups +- **Fix**: Add secondary indices `runningTasksByType: Map>` +- **Impact**: O(1) lookups, faster with 1000+ tasks + +--- + +## MEDIUM PRIORITY OPTIMIZATIONS + +### 6. Pattern Test Result Caching +- **Files**: `src/inner-loop-tracker.ts:529-543` +- **Issue**: Multiple pattern tests on same data without caching +- **Fix**: Use `.exec()` once and reuse result +- **Impact**: 2-3x faster todo detection + +### 7. Dynamic Terminal Batching +- **Files**: `src/web/server.ts:46, 1350-1360` +- **Issue**: 16ms flush interval is too long for interactive sessions +- **Fix**: Flush immediately if batch > 1KB OR timeout 16ms +- **Impact**: Snappier UI feel + +### 8. Buffer Pagination +- **Files**: `src/session.ts:314-365`, `src/web/server.ts:219-224` +- **Issue**: Full buffer sent on every state update +- **Fix**: Add `/api/sessions/:id/history?offset=X&limit=Y` +- **Impact**: 10-100x faster reconnects + +### 9. Remove Unused Regex Patterns +- **Files**: `src/respawn-controller.ts:52-59` +- **Issue**: ANSI_ESCAPE_PATTERN, WHITESPACE_PATTERN marked @deprecated but still compiled +- **Fix**: Remove unused patterns +- **Impact**: Cleaner code, slightly faster startup + +### 10. State Serialization +- **Files**: `src/state-store.ts:80, 196`, `src/web/server.ts:1343` +- **Issue**: Full JSON.stringify on every save +- **Fix**: Incremental or selective serialization +- **Impact**: 5-10x faster state saves + +--- + +## LOW PRIORITY OPTIMIZATIONS + +### 11. Error Handling Consistency +- **Files**: `src/screen-manager.ts:101-104`, `src/session.ts:726-729`, `src/web/server.ts:838-845` +- **Issue**: Mix of try-catch and error codes +- **Fix**: Create ErrorRegistry, standardize responses +- **Impact**: Better debugging + +### 12. TypeScript Strict Typing +- **Files**: `src/task-tracker.ts:179`, `src/web/server.ts:138, 174` +- **Issue**: Some implicit `any` types +- **Fix**: Strong typing for all messages and payloads +- **Impact**: Better IDE support, fewer runtime errors + +### 13. Dead Code Removal +- **Files**: `src/task.ts`, `src/task-queue.ts`, `src/ralph-loop.ts` +- **Issue**: Potentially unused files +- **Fix**: Verify imports, remove unused +- **Impact**: Cleaner codebase + +### 14. Console Logging +- **Files**: Throughout `src/session.ts`, `src/respawn-controller.ts`, `src/screen-manager.ts` +- **Issue**: Debug logs not disabled in production +- **Fix**: Use logger abstraction with levels +- **Impact**: Cleaner logs + +### 15. Magic Numbers +- **Files**: `src/session.ts:397, 428`, `src/respawn-controller.ts:614, 619` +- **Issue**: Hardcoded timeout values +- **Fix**: Extract to named constants +- **Impact**: Easier tuning + +--- + +## Implementation Phases + +### Phase 1 (Critical) - Items #1-5 +- Time: 8-12 hours +- Expected Gain: 30% performance + +### Phase 2 (Important) - Items #6-10 +- Time: 6-8 hours +- Expected Gain: 15% performance + +### Phase 3 (Polish) - Items #11-15 +- Time: 4-6 hours +- Expected Gain: 5% + code quality + +--- + +## Progress Tracking + +| Item | Status | Completed By | Notes | +|------|--------|--------------|-------| +| 1 | ☐ Pending | | | +| 2 | ☐ Pending | | | +| 3 | ☐ Pending | | | +| 4 | ☐ Pending | | | +| 5 | ☐ Pending | | | +| 6 | ☐ Pending | | | +| 7 | ☐ Pending | | | +| 8 | ☐ Pending | | | +| 9 | ☐ Pending | | | +| 10 | ☐ Pending | | | +| 11 | ☐ Pending | | | +| 12 | ☐ Pending | | | +| 13 | ☐ Pending | | | +| 14 | ☐ Pending | | | +| 15 | ☐ Pending | | | diff --git a/src/web/public/app.js b/src/web/public/app.js index 4fa0900b..e133e5c4 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -283,12 +283,32 @@ class ClaudemanApp { } }); - this.eventSource.addEventListener('session:clearTerminal', (e) => { + this.eventSource.addEventListener('session:clearTerminal', async (e) => { const data = JSON.parse(e.data); if (data.id === this.activeSessionId) { - // Clear terminal after screen attach to remove initialization blank space - this.terminal.clear(); - this.terminal.reset(); + // Fetch buffer, clear terminal, write buffer, resize (no Ctrl+L needed) + try { + const res = await fetch(`/api/sessions/${data.id}/terminal`); + const termData = await res.json(); + + this.terminal.clear(); + this.terminal.reset(); + if (termData.terminalBuffer) { + this.terminal.write(termData.terminalBuffer); + } + + // Send resize to ensure proper dimensions + const dims = this.fitAddon.proposeDimensions(); + if (dims) { + await fetch(`/api/sessions/${data.id}/resize`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) + }); + } + } catch (err) { + console.error('clearTerminal refresh failed:', err); + } } }); @@ -669,15 +689,6 @@ class ClaudemanApp { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) }); - // Send Ctrl+L to fix squished display after tab switch - const session = this.sessions.get(sessionId); - if (session && session.mode !== 'shell') { - fetch(`/api/sessions/${sessionId}/input`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ input: '\x0c' }) - }); - } } // Update respawn banner @@ -858,7 +869,7 @@ class ClaudemanApp { incrementTabCount() { const input = document.getElementById('tabCount'); const current = parseInt(input.value) || 1; - input.value = Math.min(10, current + 1); + input.value = Math.min(20, current + 1); } decrementTabCount() { @@ -869,7 +880,7 @@ class ClaudemanApp { async runClaude() { const caseName = document.getElementById('quickStartCase').value || 'testcase'; - const tabCount = Math.min(10, Math.max(1, parseInt(document.getElementById('tabCount').value) || 1)); + const tabCount = Math.min(20, Math.max(1, parseInt(document.getElementById('tabCount').value) || 1)); this.terminal.clear(); this.terminal.writeln(`\x1b[1;32m Starting ${tabCount} Claude session(s) in ${caseName}...\x1b[0m`); @@ -947,35 +958,9 @@ class ClaudemanApp { this.terminal.writeln(`\x1b[90m Created session ${i}/${tabCount}: ${sessionName}\x1b[0m`); } - // Auto-switch to the new session + // Auto-switch to the new session using selectSession (does proper refresh) if (firstSessionId) { - // IMPORTANT: Clear terminal BEFORE setting activeSessionId - // This prevents SSE events from writing during the transition - this.terminal.clear(); - this.terminal.reset(); - - // Small delay to ensure terminal is fully reset before SSE events can write - await new Promise(resolve => setTimeout(resolve, 50)); - - // NOW set the active session so SSE events start populating - this.activeSessionId = firstSessionId; - this.renderSessionTabs(); - this.renderInnerStatePanel(); // Render Ralph panel if enabled globally - - // Send resize to the new session - const dims = this.fitAddon.proposeDimensions(); - if (dims) { - await fetch(`/api/sessions/${firstSessionId}/resize`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) - }); - } - // Mark this session as needing Ctrl+L fix once Claude is running - this.pendingCtrlL = this.pendingCtrlL || new Set(); - this.pendingCtrlL.add(firstSessionId); - console.log('[DEBUG] Marked session for pending Ctrl+L:', firstSessionId); - + await this.selectSession(firstSessionId); this.loadQuickStartCases(); } @@ -1664,6 +1649,16 @@ class ClaudemanApp { document.getElementById('appSettingsShowSystemStats').checked = settings.showSystemStats ?? true; document.getElementById('appSettingsShowTokenCount').checked = settings.showTokenCount ?? true; document.getElementById('appSettingsShowMonitor').checked = settings.showMonitor ?? true; + // Claude CLI settings + const claudeModeSelect = document.getElementById('appSettingsClaudeMode'); + const allowedToolsRow = document.getElementById('allowedToolsRow'); + claudeModeSelect.value = settings.claudeMode || 'dangerously-skip-permissions'; + document.getElementById('appSettingsAllowedTools').value = settings.allowedTools || ''; + allowedToolsRow.style.display = claudeModeSelect.value === 'allowedTools' ? '' : 'none'; + // Toggle allowed tools row visibility based on mode selection + claudeModeSelect.onchange = () => { + allowedToolsRow.style.display = claudeModeSelect.value === 'allowedTools' ? '' : 'none'; + }; document.getElementById('appSettingsModal').classList.add('active'); } @@ -1681,6 +1676,9 @@ class ClaudemanApp { showSystemStats: document.getElementById('appSettingsShowSystemStats').checked, showTokenCount: document.getElementById('appSettingsShowTokenCount').checked, showMonitor: document.getElementById('appSettingsShowMonitor').checked, + // Claude CLI settings + claudeMode: document.getElementById('appSettingsClaudeMode').value, + allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(), }; // Save to localStorage diff --git a/src/web/public/index.html b/src/web/public/index.html index 057fae72..b0faf66d 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -167,7 +167,7 @@
- +