refactor: UI cleanup - move respawn controls, remove mode label, fix dir width

- Move respawn controls from toolbar to session options modal
- Remove unnecessary Mode label from session options (visible in tab)
- Increase directory input width from 200px to 350px
- Add RTL direction to directory display to show end of long paths
- Add respawn status indicator with stop/configure buttons in modal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 02:06:37 +01:00
co-authored by Claude Opus 4.5
parent 3be3595907
commit 363b6d1147
4 changed files with 127 additions and 21 deletions
+24 -7
View File
@@ -8,6 +8,8 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R
**Tech Stack**: TypeScript, Node.js, Fastify, Server-Sent Events, node-pty **Tech Stack**: TypeScript, Node.js, Fastify, Server-Sent Events, node-pty
**Requirements**: Node.js 18+, Claude CLI (`claude`) installed and available in PATH
## Commands ## Commands
```bash ```bash
@@ -18,13 +20,6 @@ npm run clean # Remove dist/
npm link # Make 'claudeman' globally available npm link # Make 'claudeman' globally available
claudeman web # Start web interface on port 3000 claudeman web # Start web interface on port 3000
claudeman web -p 8080 # Custom port claudeman web -p 8080 # Custom port
# Testing
npm run test # Run all tests once
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage report
npx vitest run test/session.test.ts # Run single test file
npx vitest run -t "should create session" # Run tests matching pattern
``` ```
## Architecture ## Architecture
@@ -314,8 +309,30 @@ GET /api/events # SSE stream (real-time events)
GET /api/status # Full state snapshot (sessions + scheduled + respawn) GET /api/status # Full state snapshot (sessions + scheduled + respawn)
``` ```
## Testing
Tests use Vitest and auto-discover `*.test.ts` files in the `test/` directory:
```bash
npm run test # Run all tests once
npm run test:watch # Watch mode
npm run test:coverage # With coverage report
npx vitest run test/session.test.ts # Single file
npx vitest run -t "should create session" # By pattern
```
## Frontend
The web UI (`src/web/public/`) uses vanilla JavaScript with:
- **xterm.js**: Terminal emulator with WebGL renderer for 60fps performance
- **xterm-addon-fit**: Auto-resize terminal to container
- **Server-Sent Events**: Real-time updates from `/api/events`
- **No build step**: Static files served directly by Fastify
## Pending Tasks ## Pending Tasks
**Note to Claude: Do NOT remove or modify this section during /init. These tasks are actively being worked on by other sessions.**
- [ ] Remove the "New Session" tab and add a gear icon (⚙️) in the top right corner for app settings - [ ] 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 - [ ] 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 - [ ] 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
+46 -1
View File
@@ -1167,14 +1167,59 @@ class ClaudemanApp {
this.editingSessionId = sessionId; this.editingSessionId = sessionId;
document.getElementById('sessionNameInput').value = session.name || ''; document.getElementById('sessionNameInput').value = session.name || '';
document.getElementById('sessionModeDisplay').textContent = session.mode === 'shell' ? 'Shell' : 'Claude';
document.getElementById('sessionDirDisplay').textContent = session.workingDir || 'Unknown'; document.getElementById('sessionDirDisplay').textContent = session.workingDir || 'Unknown';
// Update respawn status display
const respawnStatus = document.getElementById('sessionRespawnStatus');
const stopBtn = document.getElementById('sessionStopRespawnBtn');
const configBtn = document.getElementById('sessionConfigRespawnBtn');
if (this.respawnStatus[sessionId]) {
respawnStatus.classList.add('active');
respawnStatus.querySelector('.respawn-status-text').textContent =
`Active (${this.respawnStatus[sessionId].state || 'running'})`;
stopBtn.style.display = '';
configBtn.textContent = 'Reconfigure';
} else {
respawnStatus.classList.remove('active');
respawnStatus.querySelector('.respawn-status-text').textContent = 'Not active';
stopBtn.style.display = 'none';
configBtn.textContent = 'Configure Respawn';
}
// Only show respawn section for claude mode sessions with a running process
const respawnSection = document.getElementById('sessionRespawnSection');
if (session.mode === 'claude' && session.pid) {
respawnSection.style.display = '';
} else {
respawnSection.style.display = 'none';
}
document.getElementById('sessionOptionsModal').classList.add('active'); document.getElementById('sessionOptionsModal').classList.add('active');
// Focus the name input // Focus the name input
setTimeout(() => document.getElementById('sessionNameInput').focus(), 100); setTimeout(() => document.getElementById('sessionNameInput').focus(), 100);
} }
async stopRespawnFromOptions() {
if (!this.editingSessionId) return;
try {
await fetch(`/api/sessions/${this.editingSessionId}/respawn/stop`, { method: 'POST' });
delete this.respawnTimers[this.editingSessionId];
// Update the modal display
const respawnStatus = document.getElementById('sessionRespawnStatus');
respawnStatus.classList.remove('active');
respawnStatus.querySelector('.respawn-status-text').textContent = 'Not active';
document.getElementById('sessionStopRespawnBtn').style.display = 'none';
document.getElementById('sessionConfigRespawnBtn').textContent = 'Configure Respawn';
this.showToast('Respawn stopped', 'success');
} catch (err) {
this.showToast('Failed to stop respawn', 'error');
}
}
closeSessionOptions() { closeSessionOptions() {
this.editingSessionId = null; this.editingSessionId = null;
document.getElementById('sessionOptionsModal').classList.remove('active'); document.getElementById('sessionOptionsModal').classList.remove('active');
+10 -11
View File
@@ -85,13 +85,6 @@
</button> </button>
<input type="text" id="dirInput" class="toolbar-input hidden" placeholder="Working directory..." onblur="app.hideDirInput()"> <input type="text" id="dirInput" class="toolbar-input hidden" placeholder="Working directory..." onblur="app.hideDirInput()">
</div> </div>
<!-- Respawn Settings -->
<div class="toolbar-group">
<button class="btn-toolbar" onclick="app.toggleRespawnPanel()" id="respawnToggleBtn" title="Respawn settings">
Respawn
</button>
</div>
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">
@@ -257,14 +250,20 @@
<label>Session Name</label> <label>Session Name</label>
<input type="text" id="sessionNameInput" placeholder="Enter session name..."> <input type="text" id="sessionNameInput" placeholder="Enter session name...">
</div> </div>
<div class="form-row">
<label>Mode</label>
<div class="session-mode-display" id="sessionModeDisplay">Claude</div>
</div>
<div class="form-row"> <div class="form-row">
<label>Directory</label> <label>Directory</label>
<div class="session-dir-display" id="sessionDirDisplay">/path/to/dir</div> <div class="session-dir-display" id="sessionDirDisplay">/path/to/dir</div>
</div> </div>
<div class="form-row session-respawn-section" id="sessionRespawnSection">
<label>Respawn Controller</label>
<div class="session-respawn-status" id="sessionRespawnStatus">
<span class="respawn-status-text">Not active</span>
</div>
<div class="session-respawn-actions">
<button class="btn-toolbar btn-sm" onclick="app.toggleRespawnPanel(); app.closeSessionOptions();" id="sessionConfigRespawnBtn">Configure Respawn</button>
<button class="btn-toolbar btn-sm btn-danger" onclick="app.stopRespawnFromOptions()" id="sessionStopRespawnBtn" style="display: none;">Stop Respawn</button>
</div>
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button class="btn-toolbar" onclick="app.closeSessionOptions()">Cancel</button> <button class="btn-toolbar" onclick="app.closeSessionOptions()">Cancel</button>
+47 -2
View File
@@ -486,7 +486,17 @@ body {
border-radius: 4px; border-radius: 4px;
color: var(--text); color: var(--text);
font-size: 0.75rem; font-size: 0.75rem;
width: 200px; width: 350px;
max-width: 50vw;
}
#dirDisplay {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
direction: rtl;
text-align: left;
} }
.toolbar-input:focus { .toolbar-input:focus {
@@ -714,7 +724,6 @@ body {
max-width: 320px; max-width: 320px;
} }
.session-mode-display,
.session-dir-display { .session-dir-display {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
background: var(--bg-input); background: var(--bg-input);
@@ -728,6 +737,42 @@ body {
white-space: nowrap; white-space: nowrap;
} }
/* Session respawn section in options modal */
.session-respawn-section {
border-top: 1px solid var(--border);
padding-top: 0.75rem;
margin-top: 0.5rem;
}
.session-respawn-status {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
margin-bottom: 0.5rem;
}
.session-respawn-status .respawn-status-text {
font-size: 0.8rem;
color: var(--text-dim);
}
.session-respawn-status.active {
border-color: rgba(34, 197, 94, 0.4);
}
.session-respawn-status.active .respawn-status-text {
color: var(--green);
}
.session-respawn-actions {
display: flex;
gap: 0.5rem;
}
.modal-header { .modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;