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 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 04:19:34 +01:00
co-authored by Claude Opus 4.5
parent 8cfc02785f
commit ce1d871b1e
4 changed files with 242 additions and 55 deletions
+149
View File
@@ -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<string, Set<string>>`
- **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 | | |
+41 -43
View File
@@ -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
+22 -2
View File
@@ -167,7 +167,7 @@
</button>
<div class="tab-count-group" title="Number of tabs to open (1-10)">
<button class="tab-count-btn" onclick="app.decrementTabCount()">−</button>
<input type="number" id="tabCount" class="tab-count-input" value="1" min="1" max="10" readonly>
<input type="number" id="tabCount" class="tab-count-input" value="1" min="1" max="20" readonly>
<button class="tab-count-btn" onclick="app.incrementTabCount()">+</button>
</div>
<button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
@@ -479,7 +479,24 @@
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">General</div>
<div class="settings-section-title">Claude CLI</div>
<div class="form-row">
<label>Startup Mode</label>
<select id="appSettingsClaudeMode" class="form-select">
<option value="dangerously-skip-permissions">Skip Permissions (default)</option>
<option value="normal">Normal (with prompts)</option>
<option value="allowedTools">Allowed Tools Only</option>
</select>
<span class="form-hint">How Claude CLI is started in screen sessions</span>
</div>
<div class="form-row" id="allowedToolsRow" style="display: none;">
<label>Allowed Tools</label>
<input type="text" id="appSettingsAllowedTools" placeholder="Read,Glob,Grep,Bash">
<span class="form-hint">Comma-separated list of tools to allow (e.g., Read,Glob,Grep,Bash)</span>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Paths & Defaults</div>
<div class="form-row">
<label>Default CLAUDE.md Template Path</label>
<input type="text" id="appSettingsClaudeMdPath" placeholder="/path/to/CLAUDE.md template">
@@ -490,6 +507,9 @@
<input type="text" id="appSettingsDefaultDir" placeholder="~/projects">
<span class="form-hint">Default directory for new sessions.</span>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Features</div>
<div class="form-row form-row-switch">
<label>Enable Ralph Wiggum Tracker</label>
<label class="switch">
+30 -10
View File
@@ -42,9 +42,9 @@ body {
/* Compact Header */
.header {
display: flex;
align-items: center;
height: var(--header-height);
padding: 0 0.75rem;
align-items: flex-start;
min-height: var(--header-height);
padding: 0.35rem 0.75rem;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
@@ -56,6 +56,8 @@ body {
padding-right: 0.75rem;
border-right: 1px solid var(--border);
margin-right: 0.5rem;
height: var(--header-height);
flex-shrink: 0;
}
.logo {
@@ -68,12 +70,30 @@ body {
/* Session Tabs */
.session-tabs {
display: flex;
align-items: center;
align-items: flex-start;
flex-wrap: wrap;
gap: 2px;
gap: 4px 2px;
flex: 1;
overflow-x: auto;
padding: 0 0.25rem;
overflow-y: auto;
max-height: 120px;
padding: 0.15rem 0.25rem;
}
.session-tabs::-webkit-scrollbar {
width: 4px;
}
.session-tabs::-webkit-scrollbar-track {
background: transparent;
}
.session-tabs::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
.session-tabs::-webkit-scrollbar-thumb:hover {
background: var(--border-light);
}
.session-tabs::-webkit-scrollbar {
@@ -1225,9 +1245,9 @@ body {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.4rem center;
width: auto;
min-width: 60px;
max-width: 150px;
width: fit-content;
min-width: unset;
max-width: unset;
}
.toolbar-select:hover {