From 281651e553c0f224014966f29fe182d249a733a7 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 01:42:47 +0100 Subject: [PATCH] fix: improve UI usability and keyboard shortcuts - Fix terminal welcome message to say "Run Claude" instead of "Quick Start" - Fix Ctrl+Enter keyboard shortcut by using capture phase - Auto-select default case and show directory on load - Make gear icon larger (1rem) with rotation animation on hover - Add right-click on session tabs for inline rename - Update directory display when case selection changes Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 17 +++++++- src/web/public/app.js | 86 +++++++++++++++++++++++++++++++++++++-- src/web/public/index.html | 2 +- src/web/public/styles.css | 7 ++-- 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6db40cfc..e8750f08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,11 @@ src/ - Terminal buffer persisted for client reconnections - Works with RespawnController for autonomous cycling +**Shell Mode** (`startShell()`): +- Plain bash/zsh terminal without Claude +- Useful for running commands alongside Claude sessions +- Same PTY features (resize, buffer persistence) + ## Code Patterns ### Claude Message Parsing @@ -241,8 +246,9 @@ All events are broadcast to clients connected to `/api/events`. Event format: `{ ``` GET /api/sessions # List all sessions (includes buffer stats) -POST /api/sessions # Create session { workingDir } +POST /api/sessions # Create session { workingDir, mode?, name? } GET /api/sessions/:id # Get single session details +PUT /api/sessions/:id/name # Rename session { name } DELETE /api/sessions/:id # Stop and remove a session (kills process + children) DELETE /api/sessions # Kill all sessions at once GET /api/sessions/:id/output # Get session output buffer @@ -253,7 +259,8 @@ GET /api/sessions/:id/terminal # Get terminal buffer (raw ANSI) ``` POST /api/sessions/:id/run # Run prompt { prompt } (one-shot mode) -POST /api/sessions/:id/interactive # Start interactive terminal mode +POST /api/sessions/:id/interactive # Start interactive Claude terminal mode +POST /api/sessions/:id/shell # Start plain shell (bash/zsh, no Claude) POST /api/sessions/:id/input # Send input to interactive session { input } POST /api/sessions/:id/resize # Resize terminal { cols, rows } POST /api/sessions/:id/interactive-respawn # Start interactive + respawn controller @@ -307,3 +314,9 @@ GET /api/events # SSE stream (real-time events) GET /api/status # Full state snapshot (sessions + scheduled + respawn) ``` +## Pending Tasks + +- [ ] Remove the "New Session" tab and add a gear icon (⚙️) in the top right corner for app settings +- [ ] Add confirmation dialog when clicking "x" on a session tab - warn user that the screen session behind will be closed +- [ ] In settings: add option to configure a default CLAUDE.md file path (e.g. /home/arkon/default/CLAUDE.md) that will be used for new sessions/cases +- [ ] Wrap each session in a GNU screen session so they persist even if webinterface stops. Track active screen sessions in a file (~/.claudeman/screens.json), allow killing them after webserver restart, and show them in the background tasks tab for monitoring. Include a detailed task manager view: show memory usage (MB), CPU usage, child processes spawned per session - make it very responsive with real-time updates diff --git a/src/web/public/app.js b/src/web/public/app.js index b4ff5f97..ed63cb0c 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -108,7 +108,7 @@ class ClaudemanApp { showWelcome() { this.terminal.writeln('\x1b[1;36m Claudeman Terminal\x1b[0m'); - this.terminal.writeln('\x1b[90m Click "Quick Start" or press Ctrl+Enter to begin\x1b[0m'); + this.terminal.writeln('\x1b[90m Click "Run Claude" or press Ctrl+Enter to begin\x1b[0m'); this.terminal.writeln(''); } @@ -127,6 +127,7 @@ class ClaudemanApp { } setupEventListeners() { + // Use capture to handle before terminal document.addEventListener('keydown', (e) => { // Escape - close panels and modals if (e.key === 'Escape') { @@ -185,7 +186,7 @@ class ClaudemanApp { e.preventDefault(); this.decreaseFontSize(); } - }); + }, true); // Use capture phase to handle before terminal } // ========== SSE Connection ========== @@ -465,10 +466,10 @@ class ClaudemanApp { const hasRunningTasks = taskStats.running > 0; html += ` -
+
${mode === 'shell' ? 'sh' : ''} - ${this.escapeHtml(name)} + ${this.escapeHtml(name)} ${hasRunningTasks ? `${taskStats.running}` : ''} ⚙ × @@ -597,11 +598,36 @@ class ClaudemanApp { select.innerHTML = options; newSessionSelect.innerHTML = options; + + // Auto-select first case and update directory display + if (cases.length > 0) { + const firstCase = cases.find(c => c.name === 'testcase') || cases[0]; + select.value = firstCase.name; + this.updateDirDisplayForCase(firstCase.name); + } + + // Update directory when case selection changes + select.addEventListener('change', () => { + this.updateDirDisplayForCase(select.value); + }); } catch (err) { console.error('Failed to load cases:', err); } } + async updateDirDisplayForCase(caseName) { + try { + const res = await fetch(`/api/cases/${caseName}`); + const data = await res.json(); + if (data.path) { + document.getElementById('dirDisplay').textContent = data.path; + document.getElementById('dirInput').value = data.path; + } + } catch (err) { + document.getElementById('dirDisplay').textContent = caseName; + } + } + async quickStart() { // Alias for backward compatibility return this.runClaude(); @@ -1175,6 +1201,58 @@ class ClaudemanApp { } } + // Inline rename on right-click + startInlineRename(sessionId) { + const session = this.sessions.get(sessionId); + if (!session) return; + + const tabName = document.querySelector(`.tab-name[data-session-id="${sessionId}"]`); + if (!tabName) return; + + const currentName = this.getSessionName(session); + const input = document.createElement('input'); + input.type = 'text'; + input.value = session.name || ''; + input.placeholder = currentName; + input.className = 'tab-rename-input'; + input.style.cssText = 'width: 80px; font-size: 0.75rem; padding: 2px 4px; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 3px; color: var(--text); outline: none;'; + + const originalContent = tabName.textContent; + tabName.textContent = ''; + tabName.appendChild(input); + input.focus(); + input.select(); + + const finishRename = async () => { + const newName = input.value.trim(); + tabName.textContent = newName || originalContent; + + if (newName && newName !== session.name) { + try { + await fetch(`/api/sessions/${sessionId}/name`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newName }) + }); + } catch (err) { + tabName.textContent = originalContent; + this.showToast('Failed to rename', 'error'); + } + } + }; + + input.addEventListener('blur', finishRename); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + input.blur(); + } else if (e.key === 'Escape') { + input.value = ''; + input.blur(); + } + }); + } + // ========== Help Modal ========== showHelp() { diff --git a/src/web/public/index.html b/src/web/public/index.html index 3c7dd325..de1f94bf 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -217,7 +217,7 @@