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 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 01:42:47 +01:00
co-authored by Claude Opus 4.5
parent dc5c874b11
commit 281651e553
4 changed files with 102 additions and 10 deletions
+15 -2
View File
@@ -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
+82 -4
View File
@@ -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 += `
<div class="session-tab ${isActive ? 'active' : ''}" data-id="${id}" onclick="app.selectSession('${id}')">
<div class="session-tab ${isActive ? 'active' : ''}" data-id="${id}" onclick="app.selectSession('${id}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${id}')">
<span class="tab-status ${status}"></span>
${mode === 'shell' ? '<span class="tab-mode shell">sh</span>' : ''}
<span class="tab-name">${this.escapeHtml(name)}</span>
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()">${taskStats.running}</span>` : ''}
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">&#x2699;</span>
<span class="tab-close" onclick="event.stopPropagation(); app.closeSession('${id}')">&times;</span>
@@ -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() {
+1 -1
View File
@@ -217,7 +217,7 @@
</div>
<div class="modal-body">
<div class="shortcuts-grid">
<div><kbd>Ctrl</kbd>+<kbd>Enter</kbd></div><div>Quick Start</div>
<div><kbd>Ctrl</kbd>+<kbd>Enter</kbd></div><div>Run Claude</div>
<div><kbd>Ctrl</kbd>+<kbd>N</kbd></div><div>New Session</div>
<div><kbd>Ctrl</kbd>+<kbd>W</kbd></div><div>Close Session</div>
<div><kbd>Ctrl</kbd>+<kbd>Tab</kbd></div><div>Next Session</div>
+4 -3
View File
@@ -190,12 +190,12 @@ body {
/* Session tab gear icon */
.session-tab .tab-gear {
opacity: 0;
padding: 0 0.2rem;
font-size: 0.75rem;
padding: 0 0.3rem;
font-size: 1rem;
line-height: 1;
color: var(--text-muted);
cursor: pointer;
transition: opacity 0.15s;
transition: opacity 0.15s, transform 0.2s;
}
.session-tab:hover .tab-gear {
@@ -204,6 +204,7 @@ body {
.session-tab .tab-gear:hover {
color: var(--accent-hover);
transform: rotate(45deg);
}
/* Mode indicator on session tab */