mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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:
@@ -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
@@ -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">⚙</span>
|
||||
<span class="tab-close" onclick="event.stopPropagation(); app.closeSession('${id}')">×</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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user