From 23cafd2556fb9bc5e2a6fd34e958913b5c5958a7 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 10:36:30 +0100 Subject: [PATCH] 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 --- CLAUDE.md | 266 +++++--------------------------------- src/web/public/app.js | 211 +++++++++++++++++++++++++++--- src/web/public/index.html | 92 +++++++++++-- src/web/public/styles.css | 191 ++++++++++++++++++++++++++- src/web/server.ts | 11 +- 5 files changed, 499 insertions(+), 272 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 14abd781..e233fd8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,20 +33,6 @@ npx vitest run test/session.test.ts # Single file 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 ``` @@ -64,7 +50,7 @@ src/ ├── types.ts # All TypeScript interfaces ├── web/ │ ├── server.ts # Fastify REST API + SSE + session restoration -│ └── public/ # Static frontend files +│ └── public/ # Static frontend files (vanilla JS, xterm.js) └── templates/ └── claude-md.ts # CLAUDE.md generator for new cases ``` @@ -78,43 +64,19 @@ src/ ### 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 `PHRASE` markers. Supports time-aware loops with minimum duration. - -- **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.) +- **WebServer** (`src/web/server.ts`): Fastify server with REST API + SSE. All endpoints under `/api/`. See file for full route list. ### Session Modes -**One-Shot Mode** (`runPrompt(prompt)`): -- Execute a single prompt and receive completion event -- Used for scheduled runs and quick API calls -- 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) +- **One-Shot** (`runPrompt(prompt)`): Execute single prompt, receive completion event, session exits +- **Interactive** (`startInteractive()`): Persistent PTY terminal with resize support, buffer persistence +- **Shell** (`startShell()`): Plain bash/zsh terminal without Claude ## Code Patterns @@ -132,13 +94,11 @@ const msg = JSON.parse(cleanLine) as ClaudeMessage; ### PTY Spawn Modes -**One-shot mode** (prompt execution with JSON output for token tracking): ```typescript +// One-shot mode (JSON output for token tracking) pty.spawn('claude', ['-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', prompt], { ... }) -``` -**Interactive mode** (persistent terminal, tokens parsed from status line): -```typescript +// Interactive mode (tokens parsed from status line) 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. -### Long-Running Session Support +### Token Tracking -Sessions are optimized for 12-24+ hour runs with automatic buffer management: - -**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 - } -} -``` +- **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 ### 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 ``` -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 -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. -- **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: +### Tab Switching Fix +When switching Claude session tabs, terminal may be rendered at wrong size. Fix sequence: 1. Clear and reset xterm -2. Write the terminal buffer +2. Write terminal buffer 3. Send resize to update PTY dimensions -4. Send Ctrl+L (`\x0c`) to trigger Claude CLI to redraw at the correct size - -This only applies to Claude sessions (not shell sessions) since Claude CLI responds to Ctrl+L by redrawing its interface. +4. Send Ctrl+L (`\x0c`) to trigger Claude CLI redraw ### SSE Events 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. - -## 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) +Event prefixes: `session:`, `task:`, `respawn:`, `scheduled:`, `case:`, `init`. Key events: `session:idle`, `session:working`, `session:terminal`, `session:clearTerminal`, `session:completion`, `respawn:stateChanged`. ## Adding New Features ### New API Endpoint - -1. Add types to `src/types.ts` (request/response interfaces) -2. Add route in `src/web/server.ts` within the `buildServer()` function -3. Follow existing patterns: use `createErrorResponse()` for errors +1. Add types to `src/types.ts` +2. Add route in `src/web/server.ts` within `buildServer()` +3. Use `createErrorResponse()` for errors ### New SSE Event - -1. Add event type constant in `src/web/server.ts` (see `broadcast()` calls) -2. Emit from appropriate component (Session, RespawnController, etc.) -3. Handle in `src/web/public/app.js` `handleSSEEvent()` switch +1. Emit from component via `broadcast()` in server.ts +2. Handle in `src/web/public/app.js` `handleSSEEvent()` switch ### New Session Event - 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 -4. Handle in `src/web/public/app.js` SSE event listener +4. Handle in frontend SSE listener ## Notes - State persists to `~/.claudeman/state.json` and `~/.claudeman/screens.json` -- Cases are created in `~/claudeman-cases/` by default -- Sessions are wrapped in GNU screen for persistence across server restarts -- Tests use vitest with mocking via `vi.mock()` - no real Claude CLI spawned +- Cases created in `~/claudeman-cases/` by default +- Sessions wrapped in GNU screen for persistence across server restarts +- 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`) diff --git a/src/web/public/app.js b/src/web/public/app.js index 7d28bac1..507c7748 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1470,6 +1470,92 @@ class ClaudemanApp { 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 = '↗'; // 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 = '↙'; // 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() { const session = this.sessions.get(this.activeSessionId); const body = document.getElementById('backgroundTasksBody'); @@ -1561,32 +1647,103 @@ class ClaudemanApp { } } - async killAllSessions() { + killAllSessions() { const count = this.screenSessions?.length || 0; if (count === 0) { alert('No sessions to kill'); return; } - const confirmed = confirm(`Kill all ${count} session(s)? This cannot be undone.`); - if (!confirmed) return; + // Show the kill all modal + document.getElementById('killAllCount').textContent = count; + document.getElementById('killAllModal').classList.add('active'); + } + + closeKillAllModal() { + document.getElementById('killAllModal').classList.remove('active'); + } + + async confirmKillAll(killScreens) { + this.closeKillAllModal(); try { - const res = await fetch('/api/sessions', { method: 'DELETE' }); - const data = await res.json(); - if (data.success) { - // Clear local state + if (killScreens) { + // Kill everything including screens + const res = await fetch('/api/sessions', { method: 'DELETE' }); + 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.screenSessions = []; this.activeSessionId = null; this.renderSessionTabs(); - this.renderScreenSessions(); this.terminal.clear(); this.terminal.reset(); + this.toast('All tabs removed, screens still running', 'info'); } } catch (err) { - console.error('Failed to kill all sessions:', err); - alert('Failed to kill sessions: ' + err.message); + console.error('Failed to kill sessions:', err); + 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) { const cpuEl = document.getElementById('statCpu'); + const cpuBar = document.getElementById('statCpuBar'); const memEl = document.getElementById('statMem'); + const memBar = document.getElementById('statMemBar'); - if (cpuEl) { - cpuEl.textContent = `CPU: ${stats.cpu}%`; - // Color based on usage + if (cpuEl && cpuBar) { + cpuEl.textContent = `${stats.cpu}%`; + 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) { + cpuBar.classList.add('high'); cpuEl.classList.add('high'); - } else { - cpuEl.classList.remove('high'); + } else if (stats.cpu > 50) { + cpuBar.classList.add('medium'); } } - if (memEl) { + if (memEl && memBar) { const memGB = (stats.memory.usedMB / 1024).toFixed(1); - memEl.textContent = `Mem: ${memGB}GB`; - // Color based on usage + memEl.textContent = `${memGB}G`; + 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) { + memBar.classList.add('high'); memEl.classList.add('high'); - } else { - memEl.classList.remove('high'); + } else if (stats.memory.percent > 50) { + memBar.classList.add('medium'); } } } diff --git a/src/web/public/index.html b/src/web/public/index.html index a76c7259..b7582b4c 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -36,8 +36,20 @@
0 tokens
- CPU: -- - Mem: -- +
+ CPU +
+
+
+ --% +
+
+ MEM +
+
+
+ -- +
@@ -74,7 +86,7 @@
-
@@ -82,12 +94,15 @@
- - +
+ + +
@@ -117,7 +132,7 @@
Ctrl+Enter
Run Claude
Ctrl+W
Close Session
Ctrl+Tab
Next Session
-
Ctrl+K
Kill All Sessions
+
Ctrl+K
Kill All Sessions + Screens
Ctrl+L
Clear Terminal
Ctrl++
Increase Font
Ctrl+-
Decrease Font
@@ -130,14 +145,16 @@
-
+
Monitor
- + +
+
@@ -312,6 +329,59 @@
+ + + + + +
diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 08121ed1..57e3ca3b 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -190,7 +190,7 @@ body { .header-system-stats { display: flex; gap: 0.75rem; - font-size: 0.7rem; + font-size: 0.65rem; font-family: 'SF Mono', Monaco, monospace; color: var(--text-dim); padding: 0 0.5rem; @@ -198,11 +198,64 @@ body { margin-left: 0.5rem; } -.header-system-stats span { - white-space: nowrap; +.stat-item { + 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); font-weight: 500; } @@ -482,6 +535,47 @@ body { 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 { background: var(--green); border-color: var(--green); @@ -523,12 +617,17 @@ body { color: var(--text); } +/* Case select group with + button */ +.case-select-group { + display: flex; + align-items: center; +} + .toolbar-select { padding: 0.4rem 1.5rem 0.4rem 0.5rem; background: var(--bg-input); border: 1px solid var(--border); - border-left: none; - border-radius: 0 4px 4px 0; + border-radius: 4px 0 0 4px; color: var(--text-dim); font-size: 0.75rem; cursor: pointer; @@ -548,6 +647,29 @@ body { 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 { padding: 0.4rem 0.5rem; background: var(--bg-input); @@ -1528,6 +1650,39 @@ kbd { 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 { display: flex; align-items: center; @@ -1551,6 +1706,30 @@ kbd { 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 { border-bottom: 1px solid var(--border); } diff --git a/src/web/server.ts b/src/web/server.ts index a5a05b95..4a24596f 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -936,11 +936,20 @@ export class WebServer extends EventEmitter { // Helper to get custom CLAUDE.md template path from settings private getDefaultClaudeMdPath(): string | undefined { const settingsPath = join(homedir(), '.claudeman', 'settings.json'); + // Default template path (can be overridden in settings) + const defaultPath = '/home/arkon/default/CLAUDE.md'; + try { if (existsSync(settingsPath)) { const content = readFileSync(settingsPath, 'utf-8'); 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) { console.error('Failed to read settings:', err);