feat: UI improvements - buttons, monitor panel, system stats

- Redesign Run Claude button with purple gradient
- Redesign Run Shell button with dark green gradient
- Add + button to create new cases from toolbar
- Add Create Case modal for quick case creation
- Add Kill All modal with two options (tabs only vs full kill)
- Add CPU/Memory progress bars with color gradients
- Make monitor panel detachable as floating draggable window
- Monitor panel resizable when detached (min 350x250)
- Default CLAUDE.md template now uses /home/arkon/default/CLAUDE.md
- Streamline CLAUDE.md documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 10:36:30 +01:00
co-authored by Claude Opus 4.5
parent 6a5beb3afa
commit 23cafd2556
5 changed files with 499 additions and 272 deletions
+33 -233
View File
@@ -33,20 +33,6 @@ npx vitest run test/session.test.ts # Single file
npx vitest run -t "should create session" # By pattern npx vitest run -t "should create session" # By pattern
``` ```
### Test Files
| File | Coverage |
|------|----------|
| `session.test.ts` | Session creation, PTY modes, token tracking |
| `respawn-controller.test.ts` | State machine transitions, config updates |
| `scheduled-runs.test.ts` | Timed runs, iteration cleanup |
| `quick-start.test.ts` | Case creation + session startup |
| `sse-events.test.ts` | Event broadcasting, client reconnection |
| `integration-flows.test.ts` | Multi-step workflows |
| `edge-cases.test.ts` | Error handling, boundary conditions |
| `session-cleanup.test.ts` | Process termination, buffer management |
| `pty-interactive.test.ts` | Terminal resize, input handling |
## Architecture ## Architecture
``` ```
@@ -64,7 +50,7 @@ src/
├── types.ts # All TypeScript interfaces ├── types.ts # All TypeScript interfaces
├── web/ ├── web/
│ ├── server.ts # Fastify REST API + SSE + session restoration │ ├── server.ts # Fastify REST API + SSE + session restoration
│ └── public/ # Static frontend files │ └── public/ # Static frontend files (vanilla JS, xterm.js)
└── templates/ └── templates/
└── claude-md.ts # CLAUDE.md generator for new cases └── claude-md.ts # CLAUDE.md generator for new cases
``` ```
@@ -78,43 +64,19 @@ src/
### Key Components ### Key Components
- **Session** (`src/session.ts`): Wraps Claude CLI as PTY subprocess. Two modes: `runPrompt(prompt)` for one-shot execution, `startInteractive()` for persistent terminal. Emits `output`, `terminal`, `message`, `completion`, `exit`, `idle`, `working`, `autoClear`, `clearTerminal` events. Maintains terminal buffer for reconnections. Includes buffer management for long-running sessions (12-24+ hours) with automatic trimming. Tracks input/output tokens and supports auto-clear at configurable threshold. - **Session** (`src/session.ts`): Wraps Claude CLI as PTY subprocess. Two modes: `runPrompt(prompt)` for one-shot, `startInteractive()` for persistent terminal. Emits `output`, `terminal`, `message`, `completion`, `exit`, `idle`, `working`, `autoClear`, `clearTerminal` events.
- **TaskTracker** (`src/task-tracker.ts`): Detects Claude's background Task tool usage from JSON output. Builds a tree of parent-child task relationships. Emits `taskCreated`, `taskUpdated`, `taskCompleted`, `taskFailed` events. Used by Session to track background work. - **RespawnController** (`src/respawn-controller.ts`): State machine that keeps interactive sessions productive. Detects idle → sends update prompt → optionally `/clear` → optionally `/init` → repeats.
- **RespawnController** (`src/respawn-controller.ts`): State machine that keeps interactive sessions productive. Detects idle → sends update prompt → optionally `/clear` → optionally `/init` → repeats. Configurable timeouts, prompts, and step toggles. - **ScreenManager** (`src/screen-manager.ts`): Manages GNU screen sessions for persistent terminals. Screens survive server restarts.
- **RalphLoop** (`src/ralph-loop.ts`): Autonomous task assignment controller. Monitors sessions for idle state, assigns tasks from queue, detects completion via `<promise>PHRASE</promise>` markers. Supports time-aware loops with minimum duration. - **WebServer** (`src/web/server.ts`): Fastify server with REST API + SSE. All endpoints under `/api/`. See file for full route list.
- **WebServer** (`src/web/server.ts`): Fastify server with REST API + SSE. Manages sessions, scheduled runs, respawn controllers, and case directories. Broadcasts all events to connected clients. Restores screen sessions on startup.
- **ScreenManager** (`src/screen-manager.ts`): Manages GNU screen sessions for persistent terminals. Tracks screens in `~/.claudeman/screens.json`. Provides process stats (memory, CPU, children) and reconciliation for dead screens. Screens survive server restarts.
### Type Definitions
All TypeScript interfaces are centralized in `src/types.ts`:
- `SessionState`, `TaskState`, `RalphLoopState` - Core state types
- `RespawnConfig`, `AppConfig` - Configuration types
- `ApiErrorCode`, `createErrorResponse()` - Consistent API error handling
- Request/Response types for API endpoints (`CreateSessionRequest`, `QuickStartResponse`, etc.)
### Session Modes ### Session Modes
**One-Shot Mode** (`runPrompt(prompt)`): - **One-Shot** (`runPrompt(prompt)`): Execute single prompt, receive completion event, session exits
- Execute a single prompt and receive completion event - **Interactive** (`startInteractive()`): Persistent PTY terminal with resize support, buffer persistence
- Used for scheduled runs and quick API calls - **Shell** (`startShell()`): Plain bash/zsh terminal without Claude
- Session exits after prompt completes
**Interactive Mode** (`startInteractive()`):
- Persistent PTY terminal with full Claude CLI access
- Supports terminal resize for proper formatting
- 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 ## Code Patterns
@@ -132,13 +94,11 @@ const msg = JSON.parse(cleanLine) as ClaudeMessage;
### PTY Spawn Modes ### PTY Spawn Modes
**One-shot mode** (prompt execution with JSON output for token tracking):
```typescript ```typescript
// One-shot mode (JSON output for token tracking)
pty.spawn('claude', ['-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', prompt], { ... }) pty.spawn('claude', ['-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', prompt], { ... })
```
**Interactive mode** (persistent terminal, tokens parsed from status line): // Interactive mode (tokens parsed from status line)
```typescript
pty.spawn('claude', ['--dangerously-skip-permissions'], { ... }) pty.spawn('claude', ['--dangerously-skip-permissions'], { ... })
``` ```
@@ -146,44 +106,10 @@ pty.spawn('claude', ['--dangerously-skip-permissions'], { ... })
Session detects idle by watching for prompt character (`❯` or `\u276f`) and waiting 2 seconds without activity. RespawnController uses the same patterns plus spinner characters to detect working state. Session detects idle by watching for prompt character (`❯` or `\u276f`) and waiting 2 seconds without activity. RespawnController uses the same patterns plus spinner characters to detect working state.
### Long-Running Session Support ### Token Tracking
Sessions are optimized for 12-24+ hour runs with automatic buffer management: - **One-shot mode**: Uses `--output-format stream-json` for detailed token usage from JSON
- **Interactive mode**: Parses tokens from Claude's status line (e.g., "123.4k tokens"), estimates 60/40 input/output split
**Buffer Limits:**
- Terminal buffer: 5MB max, trims to 4MB when exceeded
- Text output: 2MB max, trims to 1.5MB when exceeded
- Messages: 1000 max, keeps most recent 800 when exceeded
- Line buffer: 64KB max with 100ms periodic flush (prevents unbounded growth from long lines)
- Completed tasks: 100 max in TaskTracker (auto-removes oldest)
- Respawn terminal buffer: 1MB max, trims to 512KB
**Performance Optimizations:**
- Server-side terminal batching at 60fps (16ms intervals)
- SSE event batching: `session:output` at 50ms, `task:updated` at 100ms
- Client-side requestAnimationFrame batching for smooth rendering
- Frontend `renderSessionTabs()` debounced at 100ms
- Parallel screen stats fetching via `Promise.all()`
- Configurable xterm scrollback (default 5000 lines)
- Buffer statistics available via session details for monitoring
**Automatic Cleanup:**
- Scheduled runs auto-deleted after 1 hour of completion
- Sessions cleaned up after each scheduled run iteration (prevents leaks)
**Buffer Stats Response:**
```typescript
{
bufferStats: {
terminalBufferSize: number; // Current terminal buffer size in bytes
textOutputSize: number; // Current text output size in bytes
messageCount: number; // Number of parsed messages
maxTerminalBuffer: number; // Max allowed terminal buffer
maxTextOutput: number; // Max allowed text output
maxMessages: number; // Max allowed messages
}
}
```
### Respawn Controller State Machine ### Respawn Controller State Machine
@@ -191,174 +117,48 @@ Sessions are optimized for 12-24+ hour runs with automatic buffer management:
WATCHING → SENDING_UPDATE → WAITING_UPDATE → SENDING_CLEAR → WAITING_CLEAR → SENDING_INIT → WAITING_INIT → WATCHING WATCHING → SENDING_UPDATE → WAITING_UPDATE → SENDING_CLEAR → WAITING_CLEAR → SENDING_INIT → WAITING_INIT → WATCHING
``` ```
Default config (`RespawnConfig` in `src/types.ts`):
- `idleTimeoutMs`: 5000 (5s after prompt)
- `updatePrompt`: "update all the docs and CLAUDE.md"
- `interStepDelayMs`: 1000 (1s between steps)
- `sendClear`: true (send /clear after update)
- `sendInit`: true (send /init after /clear)
### Token Tracking & Auto-Clear
Session tracks input/output tokens differently depending on mode:
**One-shot mode (`runPrompt`)**: Uses `--output-format stream-json` to get JSON output with detailed token usage from `msg.message.usage.input_tokens` and `output_tokens`.
**Interactive mode (`startInteractive`)**: Parses tokens from Claude's status line display (e.g., "123.4k tokens"). Since only total is shown, estimates 60/40 input/output split.
```typescript
{
tokens: {
input: number; // Total input tokens used
output: number; // Total output tokens used
total: number; // Combined total
},
autoClear: {
enabled: boolean; // Whether auto-clear is active
threshold: number; // Token threshold (default 100000)
}
}
```
When enabled, auto-clear waits for idle state, sends `/clear`, and resets token counts.
### Screen Session Initialization ### Screen Session Initialization
GNU screen creates blank space at the top when initializing a session. This is handled after attaching to the screen: GNU screen creates blank space at top when initializing. After attaching:
- **Claude sessions**: Clear buffer, emit `clearTerminal` event, client clears xterm
- **Shell sessions**: Clear buffer, send `clear\n` command
- **Claude sessions**: After 100ms, clears terminal buffer and emits `clearTerminal` event. Client receives `session:clearTerminal` via SSE and clears/resets its xterm. ### Tab Switching Fix
- **Shell sessions**: After 100ms, clears terminal buffer and sends `clear\n` command to the shell.
### Tab Switching
When switching between Claude session tabs, the terminal buffer may have been rendered at a different terminal size. To fix the "squished" display:
When switching Claude session tabs, terminal may be rendered at wrong size. Fix sequence:
1. Clear and reset xterm 1. Clear and reset xterm
2. Write the terminal buffer 2. Write terminal buffer
3. Send resize to update PTY dimensions 3. Send resize to update PTY dimensions
4. Send Ctrl+L (`\x0c`) to trigger Claude CLI to redraw at the correct size 4. Send Ctrl+L (`\x0c`) to trigger Claude CLI redraw
This only applies to Claude sessions (not shell sessions) since Claude CLI responds to Ctrl+L by redrawing its interface.
### SSE Events ### SSE Events
All events broadcast to `/api/events` with format: `{ type: string, sessionId?: string, data: any }`. All events broadcast to `/api/events` with format: `{ type: string, sessionId?: string, data: any }`.
Event categories (prefixes): `session:`, `task:`, `respawn:`, `scheduled:`, `case:`, `init`. Key events include `session:idle`, `session:working`, `session:terminal`, `session:clearTerminal`, `session:completion`, `respawn:stateChanged`. See `src/web/server.ts` for the full event catalog. Event prefixes: `session:`, `task:`, `respawn:`, `scheduled:`, `case:`, `init`. Key events: `session:idle`, `session:working`, `session:terminal`, `session:clearTerminal`, `session:completion`, `respawn:stateChanged`.
## API Endpoints
REST API served by Fastify at `src/web/server.ts`. All endpoints are under `/api/`.
**Sessions:**
- `GET /api/sessions` - List all sessions
- `POST /api/sessions` - Create session `{ workingDir, mode?, name? }`
- `GET /api/sessions/:id` - Get session details (includes bufferStats)
- `GET /api/sessions/:id/output` - Get text output buffer
- `DELETE /api/sessions/:id` - Kill and remove session
- `DELETE /api/sessions` - Kill all sessions
- `PUT /api/sessions/:id/name` - Rename session `{ name }`
- `POST /api/sessions/:id/interactive` - Start interactive Claude terminal
- `POST /api/sessions/:id/shell` - Start shell terminal (no Claude)
- `POST /api/sessions/:id/input` - Send input to session `{ input }`
- `POST /api/sessions/:id/resize` - Resize terminal `{ cols, rows }`
- `POST /api/sessions/:id/run` - Run one-shot prompt `{ prompt }`
- `GET /api/sessions/:id/terminal` - Get terminal buffer
**Respawn Controller:**
- `GET /api/sessions/:id/respawn` - Get respawn state
- `POST /api/sessions/:id/respawn/start` - Start respawn `{ config? }`
- `POST /api/sessions/:id/respawn/stop` - Stop respawn
- `PUT /api/sessions/:id/respawn/config` - Update config
- `POST /api/sessions/:id/respawn/enable` - Enable on running session `{ config?, durationMinutes? }`
- `POST /api/sessions/:id/auto-clear` - Configure auto-clear `{ enabled, threshold? }`
**Cases & Quick Start:**
- `GET /api/cases` - List cases in `~/claudeman-cases/`
- `POST /api/cases` - Create case `{ name, description? }`
- `GET /api/cases/:name` - Get case info
- `POST /api/quick-start` - Create case + interactive session `{ caseName? }`
**Scheduled Runs:**
- `GET /api/scheduled` - List scheduled runs
- `POST /api/scheduled` - Create scheduled run `{ prompt, workingDir?, durationMinutes }`
- `GET /api/scheduled/:id` - Get run status
- `DELETE /api/scheduled/:id` - Cancel run
**Screen Management:**
- `GET /api/screens` - List screen sessions with stats
- `DELETE /api/screens/:sessionId` - Kill screen session
- `POST /api/screens/reconcile` - Clean up dead screens
- `POST /api/screens/stats/start` - Start resource monitoring
- `POST /api/screens/stats/stop` - Stop resource monitoring
**System:**
- `GET /api/system/stats` - Get CPU and memory usage `{ cpu, memory: { usedMB, totalMB, percent } }`
**Other:**
- `GET /api/events` - SSE stream for real-time updates
- `GET /api/status` - Full state snapshot
- `GET /api/settings` - Get app settings
- `PUT /api/settings` - Update settings
- `POST /api/run` - Quick run prompt without creating persistent session `{ prompt, workingDir? }`
## E2E Testing with agent-browser
For UI testing, use [agent-browser](https://github.com/vercel-labs/agent-browser). A full E2E test plan is documented in `.claude/skills/e2e-test.md`.
```bash
# Setup
npx agent-browser install # Download Chromium (one-time)
npx tsx src/index.ts web & # Start server
# Basic test flow
npx agent-browser open http://localhost:3000
npx agent-browser snapshot # Get accessibility tree with element refs
npx agent-browser click @e5 # Click by element ref
npx agent-browser find text "Run Claude" click # Or use semantic locators
npx agent-browser screenshot /tmp/test.png
npx agent-browser close
```
## Frontend
The web UI (`src/web/public/`) uses vanilla JavaScript with:
- **xterm.js**: Terminal emulator with configurable scrollback (default 5000 lines)
- **xterm-addon-fit**: Auto-resize terminal to container
- **Server-Sent Events**: Real-time updates from `/api/events`
- **System Stats**: CPU and memory usage displayed in header (2s polling)
- **No build step**: Static files served directly by Fastify
Key files:
- `app.js` - Main application logic, SSE handling, session management
- `index.html` - Single page with embedded styles
- `styles.css` - All CSS styles
- Libraries loaded from CDN (xterm.js, addons)
## Adding New Features ## Adding New Features
### New API Endpoint ### New API Endpoint
1. Add types to `src/types.ts`
1. Add types to `src/types.ts` (request/response interfaces) 2. Add route in `src/web/server.ts` within `buildServer()`
2. Add route in `src/web/server.ts` within the `buildServer()` function 3. Use `createErrorResponse()` for errors
3. Follow existing patterns: use `createErrorResponse()` for errors
### New SSE Event ### New SSE Event
1. Emit from component via `broadcast()` in server.ts
1. Add event type constant in `src/web/server.ts` (see `broadcast()` calls) 2. Handle in `src/web/public/app.js` `handleSSEEvent()` switch
2. Emit from appropriate component (Session, RespawnController, etc.)
3. Handle in `src/web/public/app.js` `handleSSEEvent()` switch
### New Session Event ### New Session Event
1. Add to `SessionEvents` interface in `src/session.ts` 1. Add to `SessionEvents` interface in `src/session.ts`
2. Emit in `src/session.ts` via `this.emit()` 2. Emit via `this.emit()`
3. Subscribe in `src/web/server.ts` when wiring session to SSE 3. Subscribe in `src/web/server.ts` when wiring session to SSE
4. Handle in `src/web/public/app.js` SSE event listener 4. Handle in frontend SSE listener
## Notes ## Notes
- State persists to `~/.claudeman/state.json` and `~/.claudeman/screens.json` - State persists to `~/.claudeman/state.json` and `~/.claudeman/screens.json`
- Cases are created in `~/claudeman-cases/` by default - Cases created in `~/claudeman-cases/` by default
- Sessions are wrapped in GNU screen for persistence across server restarts - Sessions wrapped in GNU screen for persistence across server restarts
- Tests use vitest with mocking via `vi.mock()` - no real Claude CLI spawned - Tests use vitest with `vi.mock()` - no real Claude CLI spawned
- Long-running sessions (12-24+ hours) supported with automatic buffer trimming
- E2E testing available via agent-browser (see `.claude/skills/e2e-test.md`)
+190 -21
View File
@@ -1470,6 +1470,92 @@ class ClaudemanApp {
this.toggleMonitorPanel(); this.toggleMonitorPanel();
} }
// ========== Monitor Panel Detach & Drag ==========
toggleMonitorDetach() {
const panel = document.getElementById('monitorPanel');
const detachBtn = document.getElementById('monitorDetachBtn');
if (panel.classList.contains('detached')) {
// Re-attach to bottom
panel.classList.remove('detached');
panel.style.top = '';
panel.style.left = '';
panel.style.width = '';
panel.style.height = '';
if (detachBtn) {
detachBtn.innerHTML = '&#x2197;'; // Detach icon
detachBtn.title = 'Detach panel';
}
} else {
// Detach as floating window
panel.classList.add('detached');
panel.classList.add('open'); // Ensure it's visible
if (detachBtn) {
detachBtn.innerHTML = '&#x2199;'; // Attach icon
detachBtn.title = 'Attach panel';
}
// Setup drag functionality
this.setupMonitorDrag();
}
}
setupMonitorDrag() {
const panel = document.getElementById('monitorPanel');
const header = document.getElementById('monitorPanelHeader');
if (!panel || !header) return;
let isDragging = false;
let startX, startY, startLeft, startTop;
const onMouseDown = (e) => {
// Only drag from header, not from buttons
if (e.target.closest('button')) return;
if (!panel.classList.contains('detached')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// Keep within viewport bounds
const rect = panel.getBoundingClientRect();
newLeft = Math.max(0, Math.min(window.innerWidth - rect.width, newLeft));
newTop = Math.max(0, Math.min(window.innerHeight - rect.height, newTop));
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
// Remove existing listeners before adding new ones
header.removeEventListener('mousedown', header._dragHandler);
header._dragHandler = onMouseDown;
header.addEventListener('mousedown', onMouseDown);
}
renderTaskPanel() { renderTaskPanel() {
const session = this.sessions.get(this.activeSessionId); const session = this.sessions.get(this.activeSessionId);
const body = document.getElementById('backgroundTasksBody'); const body = document.getElementById('backgroundTasksBody');
@@ -1561,32 +1647,103 @@ class ClaudemanApp {
} }
} }
async killAllSessions() { killAllSessions() {
const count = this.screenSessions?.length || 0; const count = this.screenSessions?.length || 0;
if (count === 0) { if (count === 0) {
alert('No sessions to kill'); alert('No sessions to kill');
return; return;
} }
const confirmed = confirm(`Kill all ${count} session(s)? This cannot be undone.`); // Show the kill all modal
if (!confirmed) return; document.getElementById('killAllCount').textContent = count;
document.getElementById('killAllModal').classList.add('active');
}
closeKillAllModal() {
document.getElementById('killAllModal').classList.remove('active');
}
async confirmKillAll(killScreens) {
this.closeKillAllModal();
try { try {
const res = await fetch('/api/sessions', { method: 'DELETE' }); if (killScreens) {
const data = await res.json(); // Kill everything including screens
if (data.success) { const res = await fetch('/api/sessions', { method: 'DELETE' });
// Clear local state const data = await res.json();
if (data.success) {
this.sessions.clear();
this.screenSessions = [];
this.activeSessionId = null;
this.renderSessionTabs();
this.renderScreenSessions();
this.terminal.clear();
this.terminal.reset();
this.toast('All sessions and screens killed', 'success');
}
} else {
// Just remove tabs, keep screens running
this.sessions.clear(); this.sessions.clear();
this.screenSessions = [];
this.activeSessionId = null; this.activeSessionId = null;
this.renderSessionTabs(); this.renderSessionTabs();
this.renderScreenSessions();
this.terminal.clear(); this.terminal.clear();
this.terminal.reset(); this.terminal.reset();
this.toast('All tabs removed, screens still running', 'info');
} }
} catch (err) { } catch (err) {
console.error('Failed to kill all sessions:', err); console.error('Failed to kill sessions:', err);
alert('Failed to kill sessions: ' + err.message); this.toast('Failed to kill sessions: ' + err.message, 'error');
}
}
// ========== Create Case Modal ==========
showCreateCaseModal() {
document.getElementById('newCaseName').value = '';
document.getElementById('newCaseDescription').value = '';
document.getElementById('createCaseModal').classList.add('active');
document.getElementById('newCaseName').focus();
}
closeCreateCaseModal() {
document.getElementById('createCaseModal').classList.remove('active');
}
async createCase() {
const name = document.getElementById('newCaseName').value.trim();
const description = document.getElementById('newCaseDescription').value.trim();
if (!name) {
this.toast('Please enter a case name', 'error');
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
this.toast('Invalid name. Use only letters, numbers, hyphens, underscores.', 'error');
return;
}
try {
const res = await fetch('/api/cases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description })
});
const data = await res.json();
if (data.success) {
this.closeCreateCaseModal();
this.toast(`Case "${name}" created`, 'success');
// Reload cases and select the new one
await this.loadQuickStartCases();
document.getElementById('quickStartCase').value = name;
this.updateDirDisplayForCase(name);
} else {
this.toast(data.error || 'Failed to create case', 'error');
}
} catch (err) {
console.error('Failed to create case:', err);
this.toast('Failed to create case: ' + err.message, 'error');
} }
} }
@@ -1701,26 +1858,38 @@ class ClaudemanApp {
updateSystemStatsDisplay(stats) { updateSystemStatsDisplay(stats) {
const cpuEl = document.getElementById('statCpu'); const cpuEl = document.getElementById('statCpu');
const cpuBar = document.getElementById('statCpuBar');
const memEl = document.getElementById('statMem'); const memEl = document.getElementById('statMem');
const memBar = document.getElementById('statMemBar');
if (cpuEl) { if (cpuEl && cpuBar) {
cpuEl.textContent = `CPU: ${stats.cpu}%`; cpuEl.textContent = `${stats.cpu}%`;
// Color based on usage cpuBar.style.width = `${Math.min(100, stats.cpu)}%`;
// Color classes based on usage
cpuBar.classList.remove('medium', 'high');
cpuEl.classList.remove('high');
if (stats.cpu > 80) { if (stats.cpu > 80) {
cpuBar.classList.add('high');
cpuEl.classList.add('high'); cpuEl.classList.add('high');
} else { } else if (stats.cpu > 50) {
cpuEl.classList.remove('high'); cpuBar.classList.add('medium');
} }
} }
if (memEl) { if (memEl && memBar) {
const memGB = (stats.memory.usedMB / 1024).toFixed(1); const memGB = (stats.memory.usedMB / 1024).toFixed(1);
memEl.textContent = `Mem: ${memGB}GB`; memEl.textContent = `${memGB}G`;
// Color based on usage memBar.style.width = `${Math.min(100, stats.memory.percent)}%`;
// Color classes based on usage
memBar.classList.remove('medium', 'high');
memEl.classList.remove('high');
if (stats.memory.percent > 80) { if (stats.memory.percent > 80) {
memBar.classList.add('high');
memEl.classList.add('high'); memEl.classList.add('high');
} else { } else if (stats.memory.percent > 50) {
memEl.classList.remove('high'); memBar.classList.add('medium');
} }
} }
} }
+81 -11
View File
@@ -36,8 +36,20 @@
</div> </div>
<div class="header-tokens" id="headerTokens" title="Total tokens across all sessions">0 tokens</div> <div class="header-tokens" id="headerTokens" title="Total tokens across all sessions">0 tokens</div>
<div class="header-system-stats" id="headerSystemStats" title="System resource usage"> <div class="header-system-stats" id="headerSystemStats" title="System resource usage">
<span class="stat-cpu" id="statCpu">CPU: --</span> <div class="stat-item">
<span class="stat-mem" id="statMem">Mem: --</span> <span class="stat-label">CPU</span>
<div class="stat-bar">
<div class="stat-bar-fill stat-bar-cpu" id="statCpuBar"></div>
</div>
<span class="stat-value" id="statCpu">--%</span>
</div>
<div class="stat-item">
<span class="stat-label">MEM</span>
<div class="stat-bar">
<div class="stat-bar-fill stat-bar-mem" id="statMemBar"></div>
</div>
<span class="stat-value" id="statMem">--</span>
</div>
</div> </div>
<button class="btn-icon-header" onclick="app.openAppSettings()" title="App Settings">&#x2699;</button> <button class="btn-icon-header" onclick="app.openAppSettings()" title="App Settings">&#x2699;</button>
</div> </div>
@@ -74,7 +86,7 @@
<div class="toolbar-left"> <div class="toolbar-left">
<!-- Run Claude --> <!-- Run Claude -->
<div class="toolbar-group"> <div class="toolbar-group">
<button class="btn-toolbar btn-primary" onclick="app.runClaude()" title="Run Claude (Ctrl+Enter)"> <button class="btn-toolbar btn-claude" onclick="app.runClaude()" title="Run Claude (Ctrl+Enter)">
Run Claude Run Claude
</button> </button>
<div class="tab-count-group" title="Number of tabs to open (1-10)"> <div class="tab-count-group" title="Number of tabs to open (1-10)">
@@ -82,12 +94,15 @@
<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="10" readonly>
<button class="tab-count-btn" onclick="app.incrementTabCount()">+</button> <button class="tab-count-btn" onclick="app.incrementTabCount()">+</button>
</div> </div>
<button class="btn-toolbar" onclick="app.runShell()" title="Run Shell"> <button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
Shell Run Shell
</button> </button>
<select id="quickStartCase" class="toolbar-select" title="Select case"> <div class="case-select-group">
<option value="testcase">testcase</option> <select id="quickStartCase" class="toolbar-select" title="Select case">
</select> <option value="testcase">testcase</option>
</select>
<button class="btn-case-add" onclick="app.showCreateCaseModal()" title="Create new case">+</button>
</div>
</div> </div>
<!-- Directory --> <!-- Directory -->
@@ -117,7 +132,7 @@
<div><kbd>Ctrl</kbd>+<kbd>Enter</kbd></div><div>Run Claude</div> <div><kbd>Ctrl</kbd>+<kbd>Enter</kbd></div><div>Run Claude</div>
<div><kbd>Ctrl</kbd>+<kbd>W</kbd></div><div>Close 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> <div><kbd>Ctrl</kbd>+<kbd>Tab</kbd></div><div>Next Session</div>
<div><kbd>Ctrl</kbd>+<kbd>K</kbd></div><div>Kill All Sessions</div> <div><kbd>Ctrl</kbd>+<kbd>K</kbd></div><div>Kill All Sessions + Screens</div>
<div><kbd>Ctrl</kbd>+<kbd>L</kbd></div><div>Clear Terminal</div> <div><kbd>Ctrl</kbd>+<kbd>L</kbd></div><div>Clear Terminal</div>
<div><kbd>Ctrl</kbd>+<kbd>+</kbd></div><div>Increase Font</div> <div><kbd>Ctrl</kbd>+<kbd>+</kbd></div><div>Increase Font</div>
<div><kbd>Ctrl</kbd>+<kbd>-</kbd></div><div>Decrease Font</div> <div><kbd>Ctrl</kbd>+<kbd>-</kbd></div><div>Decrease Font</div>
@@ -130,14 +145,16 @@
<!-- Monitor Panel (combined Process Monitor + Background Tasks) --> <!-- Monitor Panel (combined Process Monitor + Background Tasks) -->
<div class="monitor-panel" id="monitorPanel"> <div class="monitor-panel" id="monitorPanel">
<div class="monitor-panel-header"> <div class="monitor-panel-header" id="monitorPanelHeader">
<span>Monitor</span> <span>Monitor</span>
<div class="monitor-panel-actions"> <div class="monitor-panel-actions">
<button class="btn-icon-sm btn-danger-sm" onclick="app.killAllSessions()" title="Kill all sessions">&#x2715;</button> <button class="btn-icon-sm btn-danger-sm" onclick="app.killAllSessions()" title="Kill all sessions and their screen processes">&#x2715;</button>
<button class="btn-icon-sm" onclick="app.reconcileScreens()" title="Refresh screens">&#x21BB;</button> <button class="btn-icon-sm" onclick="app.reconcileScreens()" title="Refresh screens">&#x21BB;</button>
<button class="btn-icon-sm" onclick="app.toggleMonitorDetach()" title="Detach/Attach panel" id="monitorDetachBtn">&#x2197;</button>
<button class="btn-icon-sm" onclick="app.toggleMonitorPanel()" title="Toggle panel" id="monitorToggleBtn">&#x25BC;</button> <button class="btn-icon-sm" onclick="app.toggleMonitorPanel()" title="Toggle panel" id="monitorToggleBtn">&#x25BC;</button>
</div> </div>
</div> </div>
<div class="monitor-resize-handle" id="monitorResizeHandle"></div>
<div class="monitor-panel-body"> <div class="monitor-panel-body">
<div class="monitor-section"> <div class="monitor-section">
<div class="monitor-section-header"> <div class="monitor-section-header">
@@ -312,6 +329,59 @@
</div> </div>
</div> </div>
<!-- Create Case Modal -->
<div class="modal" id="createCaseModal">
<div class="modal-backdrop" onclick="app.closeCreateCaseModal()"></div>
<div class="modal-content modal-sm">
<div class="modal-header">
<h3>Create New Case</h3>
<button class="modal-close" onclick="app.closeCreateCaseModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-row">
<label>Case Name</label>
<input type="text" id="newCaseName" placeholder="my-project" pattern="[a-zA-Z0-9_-]+">
<span class="form-hint">Letters, numbers, hyphens, underscores only</span>
</div>
<div class="form-row">
<label>Description (optional)</label>
<input type="text" id="newCaseDescription" placeholder="A brief description...">
</div>
</div>
<div class="form-actions">
<button class="btn-toolbar" onclick="app.closeCreateCaseModal()">Cancel</button>
<button class="btn-toolbar btn-primary" onclick="app.createCase()">Create</button>
</div>
</div>
</div>
<!-- Kill All Confirmation Modal -->
<div class="modal" id="killAllModal">
<div class="modal-backdrop" onclick="app.closeKillAllModal()"></div>
<div class="modal-content modal-sm">
<div class="modal-header">
<h3>Kill All Sessions</h3>
<button class="modal-close" onclick="app.closeKillAllModal()">&times;</button>
</div>
<div class="modal-body">
<p class="modal-warning-text">Choose how to close <span id="killAllCount">0</span> session(s):</p>
</div>
<div class="close-options">
<button class="close-option" onclick="app.confirmKillAll(false)">
<span class="close-option-title">Remove All Tabs</span>
<span class="close-option-desc">Screen sessions keep running in background</span>
</button>
<button class="close-option close-option-danger" onclick="app.confirmKillAll(true)">
<span class="close-option-title">Kill All Claude + Screens</span>
<span class="close-option-desc">Terminate everything completely</span>
</button>
</div>
<div class="modal-footer-cancel">
<button class="btn-toolbar btn-sm" onclick="app.closeKillAllModal()">Cancel</button>
</div>
</div>
</div>
<!-- Help Button --> <!-- Help Button -->
<button class="help-btn" onclick="app.showHelp()" title="Keyboard shortcuts (Ctrl+?)">?</button> <button class="help-btn" onclick="app.showHelp()" title="Keyboard shortcuts (Ctrl+?)">?</button>
</div> </div>
+185 -6
View File
@@ -190,7 +190,7 @@ body {
.header-system-stats { .header-system-stats {
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
font-size: 0.7rem; font-size: 0.65rem;
font-family: 'SF Mono', Monaco, monospace; font-family: 'SF Mono', Monaco, monospace;
color: var(--text-dim); color: var(--text-dim);
padding: 0 0.5rem; padding: 0 0.5rem;
@@ -198,11 +198,64 @@ body {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.header-system-stats span { .stat-item {
white-space: nowrap; display: flex;
align-items: center;
gap: 0.35rem;
} }
.header-system-stats .high { .stat-label {
color: var(--text-muted);
font-size: 0.6rem;
text-transform: uppercase;
min-width: 24px;
}
.stat-bar {
width: 40px;
height: 6px;
background: var(--bg-input);
border-radius: 3px;
overflow: hidden;
}
.stat-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease, background 0.3s ease;
}
.stat-bar-cpu {
background: linear-gradient(90deg, var(--green) 0%, var(--green) 100%);
}
.stat-bar-cpu.medium {
background: linear-gradient(90deg, var(--green) 0%, var(--yellow) 100%);
}
.stat-bar-cpu.high {
background: linear-gradient(90deg, var(--yellow) 0%, var(--red) 100%);
}
.stat-bar-mem {
background: linear-gradient(90deg, var(--accent) 0%, var(--accent-hover) 100%);
}
.stat-bar-mem.medium {
background: linear-gradient(90deg, var(--accent) 0%, var(--yellow) 100%);
}
.stat-bar-mem.high {
background: linear-gradient(90deg, var(--yellow) 0%, var(--red) 100%);
}
.stat-value {
min-width: 32px;
text-align: right;
color: var(--text-dim);
}
.stat-value.high {
color: var(--red); color: var(--red);
font-weight: 500; font-weight: 500;
} }
@@ -482,6 +535,47 @@ body {
background: var(--accent-hover); background: var(--accent-hover);
} }
/* Claude Button - Special styling */
.btn-toolbar.btn-claude {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
border-color: #3b82f6;
color: white;
font-weight: 500;
padding: 0.4rem 0.85rem;
}
.btn-toolbar.btn-claude:hover {
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
border-color: #60a5fa;
box-shadow: 0 0 12px rgba(139, 92, 246, 0.4);
}
.btn-toolbar.btn-claude .btn-icon {
font-size: 0.9rem;
}
/* Shell Button - Special styling */
.btn-toolbar.btn-shell {
background: linear-gradient(135deg, #1a2e1a 0%, #234023 100%);
border-color: #2d5a2d;
color: #8fbc8f;
font-weight: 500;
}
.btn-toolbar.btn-shell:hover {
background: linear-gradient(135deg, #234023 0%, #2d5a2d 100%);
border-color: #3d7a3d;
box-shadow: 0 0 8px rgba(34, 139, 34, 0.4);
color: #a8d8a8;
}
/* Button icon styling */
.btn-toolbar .btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-toolbar.btn-success { .btn-toolbar.btn-success {
background: var(--green); background: var(--green);
border-color: var(--green); border-color: var(--green);
@@ -523,12 +617,17 @@ body {
color: var(--text); color: var(--text);
} }
/* Case select group with + button */
.case-select-group {
display: flex;
align-items: center;
}
.toolbar-select { .toolbar-select {
padding: 0.4rem 1.5rem 0.4rem 0.5rem; padding: 0.4rem 1.5rem 0.4rem 0.5rem;
background: var(--bg-input); background: var(--bg-input);
border: 1px solid var(--border); border: 1px solid var(--border);
border-left: none; border-radius: 4px 0 0 4px;
border-radius: 0 4px 4px 0;
color: var(--text-dim); color: var(--text-dim);
font-size: 0.75rem; font-size: 0.75rem;
cursor: pointer; cursor: pointer;
@@ -548,6 +647,29 @@ body {
border-color: var(--accent); border-color: var(--accent);
} }
.btn-case-add {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: var(--bg-input);
border: 1px solid var(--border);
border-left: none;
border-radius: 0 4px 4px 0;
color: var(--green);
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.btn-case-add:hover {
background: var(--green);
color: white;
border-color: var(--green);
}
.toolbar-input { .toolbar-input {
padding: 0.4rem 0.5rem; padding: 0.4rem 0.5rem;
background: var(--bg-input); background: var(--bg-input);
@@ -1528,6 +1650,39 @@ kbd {
transform: translateY(0); transform: translateY(0);
} }
/* Detached floating state */
.monitor-panel.detached {
bottom: auto;
right: auto;
top: 100px;
left: calc(50% - 300px);
width: 600px;
height: 500px;
max-height: none;
border-radius: 8px;
border-bottom: 1px solid var(--border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
transform: none;
resize: both;
overflow: hidden;
min-width: 350px;
min-height: 250px;
}
.monitor-panel.detached .monitor-panel-header {
cursor: move;
background: linear-gradient(180deg, var(--bg-hover) 0%, var(--bg-card) 100%);
user-select: none;
}
.monitor-panel.detached .monitor-resize-handle {
display: block;
}
.monitor-panel.detached .monitor-section-body {
max-height: none;
}
.monitor-panel-header { .monitor-panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1551,6 +1706,30 @@ kbd {
padding: 0; padding: 0;
} }
/* Resize handle for detached panel */
.monitor-resize-handle {
display: none;
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, var(--border-light) 50%);
border-radius: 0 0 8px 0;
}
.monitor-resize-handle::before {
content: '';
position: absolute;
bottom: 3px;
right: 3px;
width: 8px;
height: 8px;
border-right: 2px solid var(--text-muted);
border-bottom: 2px solid var(--text-muted);
}
.monitor-section { .monitor-section {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
+10 -1
View File
@@ -936,11 +936,20 @@ export class WebServer extends EventEmitter {
// Helper to get custom CLAUDE.md template path from settings // Helper to get custom CLAUDE.md template path from settings
private getDefaultClaudeMdPath(): string | undefined { private getDefaultClaudeMdPath(): string | undefined {
const settingsPath = join(homedir(), '.claudeman', 'settings.json'); const settingsPath = join(homedir(), '.claudeman', 'settings.json');
// Default template path (can be overridden in settings)
const defaultPath = '/home/arkon/default/CLAUDE.md';
try { try {
if (existsSync(settingsPath)) { if (existsSync(settingsPath)) {
const content = readFileSync(settingsPath, 'utf-8'); const content = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(content); const settings = JSON.parse(content);
return settings.defaultClaudeMdPath || undefined; if (settings.defaultClaudeMdPath) {
return settings.defaultClaudeMdPath;
}
}
// Use default path if it exists
if (existsSync(defaultPath)) {
return defaultPath;
} }
} catch (err) { } catch (err) {
console.error('Failed to read settings:', err); console.error('Failed to read settings:', err);