diff --git a/.claude/skills/e2e-test.md b/.claude/skills/e2e-test.md new file mode 100644 index 00000000..7d6d9ca9 --- /dev/null +++ b/.claude/skills/e2e-test.md @@ -0,0 +1,85 @@ +# E2E Testing with Agent-Browser + +This skill uses agent-browser to perform end-to-end testing of the Claudeman web interface. + +## Usage + +When this skill is invoked, run comprehensive E2E tests on the web interface. + +## Test Plan + +### Prerequisites +1. Ensure the server is running: `npm run dev` or `claudeman web` +2. Default URL: http://localhost:3000 + +### Test Execution + +Use `npx agent-browser` to run the following tests: + +```bash +# Test 1: Connection and Initial Load +npx agent-browser open http://localhost:3000 +npx agent-browser wait --load networkidle +npx agent-browser snapshot +npx agent-browser screenshot /tmp/claudeman-test-1-initial.png + +# Test 2: Font Controls (A+/A-) +npx agent-browser find text "A-" click +npx agent-browser wait 500 +npx agent-browser find text "A+" click +npx agent-browser find text "A+" click +npx agent-browser screenshot /tmp/claudeman-test-2-font.png + +# Test 3: Tab Count Stepper +npx agent-browser find text "+" click # Increment tab count +npx agent-browser find text "+" click +npx agent-browser find text "−" click # Decrement +npx agent-browser screenshot /tmp/claudeman-test-3-tabcount.png + +# Test 4: Create Claude Session +npx agent-browser find text "Run Claude" click +npx agent-browser wait 2000 +npx agent-browser snapshot +npx agent-browser screenshot /tmp/claudeman-test-4-session.png + +# Test 5: Open Session Options (gear icon on tab) +npx agent-browser snapshot # Get element refs +# Find and click the gear icon on the session tab +npx agent-browser screenshot /tmp/claudeman-test-5-options.png + +# Test 6: Test Respawn Settings in Modal +# Should see: Update Prompt, Idle Timeout, Step Delay, Duration, checkboxes +npx agent-browser screenshot /tmp/claudeman-test-6-respawn.png + +# Test 7: Monitor Panel +npx agent-browser find text "Monitor" click +npx agent-browser wait 500 +npx agent-browser snapshot +npx agent-browser screenshot /tmp/claudeman-test-7-monitor.png + +# Test 8: Close Monitor Panel +npx agent-browser find text "×" click +npx agent-browser screenshot /tmp/claudeman-test-8-closed.png + +# Cleanup +npx agent-browser close +``` + +### Expected Results + +1. **Initial Load**: Page loads with header containing: logo, session tabs, font controls (A-/14/A+), connection status, tokens display, settings gear +2. **Font Controls**: Font controls should be to the LEFT of connection status +3. **Tab Count Stepper**: Nice stepper with −/number/+ buttons, not just a plain number input +4. **Session Creation**: Session tab appears, terminal shows Claude starting +5. **Session Options**: Modal opens with session name, directory, AND respawn settings (Update Prompt, timeouts, checkboxes) +6. **Monitor Panel**: Shows "Screen Sessions" and "Background Tasks" sections +7. **Respawn Settings**: All settings integrated into the session options modal, NO separate bottom panel + +### Verification Checklist + +- [ ] Font controls positioned left of connection status +- [ ] Tab count has nice stepper buttons (−/+) +- [ ] No "Respawn Settings" button in footer +- [ ] Respawn settings appear in Session Options modal +- [ ] Monitor panel opens and shows both sections +- [ ] Sessions are wrapped in GNU screen (check with `screen -ls`) diff --git a/CLAUDE.md b/CLAUDE.md index b9ee68f2..9bda16f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -322,6 +322,8 @@ GET /api/status # Full state snapshot (sessions + schedule ## Testing +### Unit Tests + Tests use Vitest and auto-discover `*.test.ts` files in the `test/` directory: ```bash @@ -332,6 +334,105 @@ npx vitest run test/session.test.ts # Single file npx vitest run -t "should create session" # By pattern ``` +### E2E Testing with agent-browser + +For UI testing, we use [agent-browser](https://github.com/vercel-labs/agent-browser) - a fast CLI for browser automation optimized for AI agents. + +**Installation:** +```bash +npm install agent-browser --save-dev +npx agent-browser install # Download Chromium +``` + +**Basic E2E Test Flow:** +```bash +# Start the server +npx tsx src/index.ts web & + +# Open browser +npx agent-browser open http://localhost:3000 + +# Get page snapshot (shows accessibility tree with element refs) +npx agent-browser snapshot + +# Click elements by reference +npx agent-browser click @e5 # Click element with ref=e5 + +# Or use semantic locators +npx agent-browser find text "Run Claude" click +npx agent-browser find role button click --name "Monitor" + +# Fill inputs +npx agent-browser fill @e32 "my-session-name" + +# Execute JavaScript in page context +npx agent-browser eval "app.sessions.get(app.activeSessionId)" + +# Take screenshots +npx agent-browser screenshot /tmp/test-result.png + +# Close browser +npx agent-browser close +``` + +**E2E Test Skill (`.claude/skills/e2e-test.md`):** + +A skill file exists that documents the full E2E test plan. Key tests include: + +1. **Initial Load**: Verify header layout (font controls, connection status, tokens) +2. **Font Controls**: Test A-/A+ buttons change font size +3. **Tab Count Stepper**: Test −/+ buttons increment/decrement +4. **Session Creation**: Create session, verify screen wrapping +5. **Session Options Modal**: Open gear icon, verify respawn settings visible +6. **Monitor Panel**: Test Screen Sessions and Background Tasks display + +**Example Test Session:** +```bash +# Clean up previous screens +screen -ls | grep -oP '\d+\.claudeman-[a-z0-9]+' | while read s; do + screen -S "$s" -X quit +done + +# Start fresh server +rm -f ~/.claudeman/screens.json +npx tsx src/index.ts web & +sleep 4 + +# Run tests +npx agent-browser open http://localhost:3000 +npx agent-browser snapshot | head -30 # Check initial state +npx agent-browser click @e5 # Run Claude +sleep 4 +npx agent-browser snapshot # Verify session created + +# Check screen wrapping +screen -ls | grep claudeman # Should show screen session + +# Check session has PID +npx agent-browser eval "app.sessions.get(app.activeSessionId).pid" + +# Test session options +npx agent-browser eval "document.querySelector('[title=\"Session options\"]').click()" +npx agent-browser snapshot | grep -E "Respawn|Enable" # Should show respawn settings + +npx agent-browser screenshot /tmp/test-final.png +npx agent-browser close +``` + +**Key Commands:** +| Command | Description | +|---------|-------------| +| `open ` | Navigate to URL | +| `snapshot` | Get accessibility tree with element refs | +| `snapshot -i` | Interactive elements only | +| `click @ref` | Click element by ref | +| `fill @ref "text"` | Fill input field | +| `eval "js code"` | Execute JavaScript | +| `screenshot path` | Save screenshot | +| `find text/role/label "x" click` | Semantic element location | +| `wait 1000` | Wait milliseconds | +| `close` | Close browser | + ## Frontend The web UI (`src/web/public/`) uses vanilla JavaScript with: diff --git a/README.md b/README.md index 4a44dde8..91f664fd 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,91 @@ npm run build npm run clean ``` +## Testing + +### Unit Tests + +```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 +``` + +### E2E Testing with agent-browser + +The project uses [agent-browser](https://github.com/vercel-labs/agent-browser) for end-to-end testing of the web interface. This tool provides browser automation via accessibility tree snapshots and element references. + +#### Installation + +```bash +npm install -g agent-browser +``` + +#### Running E2E Tests + +1. Start the web server: +```bash +npm run dev +# or +claudeman web +``` + +2. Kill any existing screen sessions (clean slate): +```bash +screen -ls | grep -oP '\d+\.\S+' | xargs -I{} screen -X -S {} quit 2>/dev/null || true +``` + +3. Run tests with agent-browser: +```bash +# Open the app and take initial snapshot +npx agent-browser open http://localhost:3000 +npx agent-browser wait --load networkidle +npx agent-browser snapshot +npx agent-browser screenshot /tmp/claudeman-test-initial.png + +# Test font controls +npx agent-browser find text "A-" click +npx agent-browser find text "A+" click + +# Test tab count stepper +npx agent-browser find text "+" click # Increment +npx agent-browser find text "−" click # Decrement + +# Test session creation +npx agent-browser find text "Run Claude" click +npx agent-browser wait 2000 +npx agent-browser snapshot + +# Test Monitor panel +npx agent-browser find text "Monitor" click +npx agent-browser wait 500 +npx agent-browser snapshot + +# Cleanup +npx agent-browser close +``` + +#### E2E Test Skill + +A skill is available at `.claude/skills/e2e-test.md` that documents the complete test plan with all test cases and expected results. Run it with Claude Code: + +```bash +/e2e-test +``` + +#### Key Test Areas + +| Area | What to Test | +|------|--------------| +| Initial Load | Header, tabs, font controls, connection status | +| Font Controls | A-/A+ buttons positioned left of connection | +| Tab Count | Stepper with −/number/+ buttons | +| Session Creation | Tab appears, terminal shows Claude starting | +| Session Options | Modal with respawn settings | +| Monitor Panel | Screen sessions and background tasks display | + ## Requirements - Node.js 18+ diff --git a/package-lock.json b/package-lock.json index 5250956f..50e431e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/node": "^20.14.0", "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^4.0.17", + "agent-browser": "^0.6.0", "tsx": "^4.15.0", "typescript": "^5.5.0", "vitest": "^4.0.17" @@ -1335,6 +1336,22 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "node_modules/agent-browser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.6.0.tgz", + "integrity": "sha512-C4Mtxfvyi/m04hyD2XdUNKc878RPZqbhXFIPEUJElZW6RhZJ1QG1/L5PSgIAhRSi4TF/EkB8iq4IAWoecMvGdg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "^1.57.0", + "ws": "^8.19.0", + "zod": "^3.22.4" + }, + "bin": { + "agent-browser": "bin/agent-browser" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -2267,6 +2284,19 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3107,6 +3137,38 @@ "engines": { "node": ">=8" } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 036dd69e..3e096d46 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/node": "^20.14.0", "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^4.0.17", + "agent-browser": "^0.6.0", "tsx": "^4.15.0", "typescript": "^5.5.0", "vitest": "^4.0.17" diff --git a/src/session.ts b/src/session.ts index b175a51d..3807df83 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,8 +1,10 @@ import { EventEmitter } from 'node:events'; import { v4 as uuidv4 } from 'uuid'; import * as pty from 'node-pty'; -import { SessionState, SessionStatus, SessionConfig } from './types.js'; +import { execSync } from 'node:child_process'; +import { SessionState, SessionStatus, SessionConfig, ScreenSession } from './types.js'; import { TaskTracker, type BackgroundTask } from './task-tracker.js'; +import { ScreenManager } from './screen-manager.js'; export type { BackgroundTask } from './task-tracker.js'; @@ -89,7 +91,18 @@ export class Session extends EventEmitter { private _autoClearEnabled: boolean = false; private _isClearing: boolean = false; // Prevent recursive clearing - constructor(config: Partial & { workingDir: string; mode?: SessionMode; name?: string }) { + // Screen session support + private _screenManager: ScreenManager | null = null; + private _screenSession: ScreenSession | null = null; + private _useScreen: boolean = false; + + constructor(config: Partial & { + workingDir: string; + mode?: SessionMode; + name?: string; + screenManager?: ScreenManager; + useScreen?: boolean; + }) { super(); this.id = config.id || uuidv4(); this.workingDir = config.workingDir; @@ -97,6 +110,8 @@ export class Session extends EventEmitter { this.mode = config.mode || 'claude'; this._name = config.name || ''; this._lastActivityAt = this.createdAt; + this._screenManager = config.screenManager || null; + this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable()); // Initialize task tracker and forward events this._taskTracker = new TaskTracker(); @@ -286,17 +301,46 @@ export class Session extends EventEmitter { this._lineBuffer = ''; this._lastActivityAt = Date.now(); - console.log('[Session] Starting interactive Claude session'); + console.log('[Session] Starting interactive Claude session' + (this._useScreen ? ' (with screen)' : '')); - this.ptyProcess = pty.spawn('claude', [ - '--dangerously-skip-permissions' - ], { - name: 'xterm-256color', - cols: 120, - rows: 40, - cwd: this.workingDir, - env: { ...process.env, TERM: 'xterm-256color' }, - }); + // If screen wrapping is enabled, create a screen session first + if (this._useScreen && this._screenManager) { + try { + this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude'); + console.log('[Session] Created screen session:', this._screenSession.screenName); + + // Wait a moment for screen to fully start + await new Promise(resolve => setTimeout(resolve, 300)); + + // Attach to the screen session via PTY + this.ptyProcess = pty.spawn('screen', [ + '-x', this._screenSession.screenName + ], { + name: 'xterm-256color', + cols: 120, + rows: 40, + cwd: this.workingDir, + env: { ...process.env, TERM: 'xterm-256color' }, + }); + } catch (err) { + console.error('[Session] Failed to create screen session, falling back to direct PTY:', err); + this._useScreen = false; + this._screenSession = null; + } + } + + // Fallback to direct PTY if screen is not used + if (!this.ptyProcess) { + this.ptyProcess = pty.spawn('claude', [ + '--dangerously-skip-permissions' + ], { + name: 'xterm-256color', + cols: 120, + rows: 40, + cwd: this.workingDir, + env: { ...process.env, TERM: 'xterm-256color' }, + }); + } this._pid = this.ptyProcess.pid; console.log('[Session] Interactive PTY spawned with PID:', this._pid); @@ -350,6 +394,10 @@ export class Session extends EventEmitter { this.ptyProcess = null; this._pid = null; this._status = 'idle'; + // If using screen, mark the screen as detached but don't kill it + if (this._screenSession && this._screenManager) { + this._screenManager.setAttached(this.id, false); + } this.emit('exit', exitCode); }); } @@ -371,15 +419,44 @@ export class Session extends EventEmitter { // Use user's default shell or bash const shell = process.env.SHELL || '/bin/bash'; - console.log('[Session] Starting shell session with:', shell); + console.log('[Session] Starting shell session with:', shell + (this._useScreen ? ' (with screen)' : '')); - this.ptyProcess = pty.spawn(shell, [], { - name: 'xterm-256color', - cols: 120, - rows: 40, - cwd: this.workingDir, - env: { ...process.env, TERM: 'xterm-256color' }, - }); + // If screen wrapping is enabled, create a screen session first + if (this._useScreen && this._screenManager) { + try { + this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell'); + console.log('[Session] Created screen session:', this._screenSession.screenName); + + // Wait a moment for screen to fully start + await new Promise(resolve => setTimeout(resolve, 300)); + + // Attach to the screen session via PTY + this.ptyProcess = pty.spawn('screen', [ + '-x', this._screenSession.screenName + ], { + name: 'xterm-256color', + cols: 120, + rows: 40, + cwd: this.workingDir, + env: { ...process.env, TERM: 'xterm-256color' }, + }); + } catch (err) { + console.error('[Session] Failed to create screen session, falling back to direct PTY:', err); + this._useScreen = false; + this._screenSession = null; + } + } + + // Fallback to direct PTY if screen is not used + if (!this.ptyProcess) { + this.ptyProcess = pty.spawn(shell, [], { + name: 'xterm-256color', + cols: 120, + rows: 40, + cwd: this.workingDir, + env: { ...process.env, TERM: 'xterm-256color' }, + }); + } this._pid = this.ptyProcess.pid; console.log('[Session] Shell PTY spawned with PID:', this._pid); @@ -406,6 +483,10 @@ export class Session extends EventEmitter { this.ptyProcess = null; this._pid = null; this._status = 'idle'; + // If using screen, mark the screen as detached but don't kill it + if (this._screenSession && this._screenManager) { + this._screenManager.setAttached(this.id, false); + } this.emit('exit', exitCode); }); diff --git a/src/web/public/app.js b/src/web/public/app.js index bcf2b65e..84f421a3 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -209,12 +209,13 @@ class ClaudemanApp { this.eventSource.addEventListener('session:updated', (e) => { const data = JSON.parse(e.data); - this.sessions.set(data.id, data); + const session = data.session || data; + this.sessions.set(session.id, session); this.renderSessionTabs(); this.updateCost(); // Update tokens display if this is the active session - if (data.id === this.activeSessionId && data.tokens) { - this.updateRespawnTokens(data.tokens.total); + if (session.id === this.activeSessionId && session.tokens) { + this.updateRespawnTokens(session.tokens.total); } }); @@ -423,7 +424,7 @@ class ClaudemanApp { this.eventSource.addEventListener('screen:statsUpdated', (e) => { this.screenSessions = JSON.parse(e.data); - if (document.getElementById('processPanel').classList.contains('open')) { + if (document.getElementById('monitorPanel').classList.contains('open')) { this.renderScreenSessions(); } }); @@ -676,6 +677,19 @@ class ClaudemanApp { return this.runClaude(); } + // Tab count stepper functions + incrementTabCount() { + const input = document.getElementById('tabCount'); + const current = parseInt(input.value) || 1; + input.value = Math.min(10, current + 1); + } + + decrementTabCount() { + const input = document.getElementById('tabCount'); + const current = parseInt(input.value) || 1; + input.value = Math.max(1, current - 1); + } + async runClaude() { const caseName = document.getElementById('quickStartCase').value || 'testcase'; const tabCount = Math.min(10, Math.max(1, parseInt(document.getElementById('tabCount').value) || 1)); @@ -824,145 +838,7 @@ class ClaudemanApp { }, 100); } - // ========== Respawn Panel ========== - - toggleRespawnPanel() { - const panel = document.getElementById('respawnPanel'); - panel.classList.toggle('open'); - - // Show "Enable on Current" button if there's an active session - const enableOnCurrentBtn = document.getElementById('enableOnCurrentBtn'); - if (this.activeSessionId && this.sessions.has(this.activeSessionId)) { - const session = this.sessions.get(this.activeSessionId); - // Only show if session is running (has a PID) - if (session.pid) { - enableOnCurrentBtn.style.display = ''; - } else { - enableOnCurrentBtn.style.display = 'none'; - } - } else { - enableOnCurrentBtn.style.display = 'none'; - } - } - - getRespawnConfig() { - const updatePrompt = document.getElementById('respawnPrompt').value; - const idleTimeout = parseInt(document.getElementById('respawnIdleTimeout').value) || 5; - const stepDelay = parseInt(document.getElementById('respawnStepDelay').value) || 1; - const sendClear = document.getElementById('respawnSendClear').checked; - const sendInit = document.getElementById('respawnSendInit').checked; - const durationStr = document.getElementById('respawnDuration').value; - const durationMinutes = durationStr ? parseInt(durationStr) : null; - const autoClearEnabled = document.getElementById('autoClearEnabled').checked; - const autoClearThreshold = parseInt(document.getElementById('autoClearThreshold').value) || 100000; - - return { - respawnConfig: { - updatePrompt, - idleTimeoutMs: idleTimeout * 1000, - interStepDelayMs: stepDelay * 1000, - sendClear, - sendInit, - }, - durationMinutes, - autoClearEnabled, - autoClearThreshold - }; - } - - async startInteractiveWithRespawn() { - this.toggleRespawnPanel(); - - const dir = document.getElementById('dirInput').value.trim(); - const { respawnConfig, durationMinutes, autoClearEnabled, autoClearThreshold } = this.getRespawnConfig(); - - this.terminal.clear(); - this.terminal.writeln('\x1b[1;32m Starting session with respawn...\x1b[0m'); - if (durationMinutes) { - this.terminal.writeln(`\x1b[90m Duration: ${durationMinutes} minutes\x1b[0m`); - } - this.terminal.writeln(''); - - try { - // Create session - const createRes = await fetch('/api/sessions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ workingDir: dir || undefined }) - }); - const createData = await createRes.json(); - if (!createData.success) throw new Error(createData.error); - - const sessionId = createData.session.id; - this.activeSessionId = sessionId; - - // Start interactive with respawn - await fetch(`/api/sessions/${sessionId}/interactive-respawn`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ respawnConfig, durationMinutes }) - }); - - // Set auto-clear if enabled - if (autoClearEnabled) { - await fetch(`/api/sessions/${sessionId}/auto-clear`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ enabled: true, threshold: autoClearThreshold }) - }); - } - - // Send resize - const dims = this.fitAddon.proposeDimensions(); - if (dims) { - await fetch(`/api/sessions/${sessionId}/resize`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) - }); - } - - this.terminal.focus(); - } catch (err) { - this.terminal.writeln(`\x1b[1;31m Error: ${err.message}\x1b[0m`); - } - } - - async enableRespawnOnCurrent() { - if (!this.activeSessionId) { - this.showToast('No active session', 'warning'); - return; - } - - this.toggleRespawnPanel(); - - const { respawnConfig, durationMinutes, autoClearEnabled, autoClearThreshold } = this.getRespawnConfig(); - - try { - // Enable respawn on existing session - const res = await fetch(`/api/sessions/${this.activeSessionId}/respawn/enable`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ config: respawnConfig, durationMinutes }) - }); - const data = await res.json(); - if (data.error) throw new Error(data.error); - - // Set auto-clear if enabled - if (autoClearEnabled) { - await fetch(`/api/sessions/${this.activeSessionId}/auto-clear`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ enabled: true, threshold: autoClearThreshold }) - }); - } - - this.showToast('Respawn enabled on current session', 'success'); - this.terminal.focus(); - } catch (err) { - this.showToast('Failed to enable respawn: ' + err.message, 'error'); - } - } + // ========== Respawn Banner ========== showRespawnBanner() { document.getElementById('respawnBanner').style.display = 'flex'; @@ -1201,22 +1077,22 @@ class ClaudemanApp { document.getElementById('sessionNameInput').value = session.name || ''; document.getElementById('sessionDirDisplay').textContent = session.workingDir || 'Unknown'; - // Update respawn status display + // Update respawn status display and buttons const respawnStatus = document.getElementById('sessionRespawnStatus'); - const stopBtn = document.getElementById('sessionStopRespawnBtn'); - const configBtn = document.getElementById('sessionConfigRespawnBtn'); + const enableBtn = document.getElementById('modalEnableRespawnBtn'); + const stopBtn = document.getElementById('modalStopRespawnBtn'); if (this.respawnStatus[sessionId]) { respawnStatus.classList.add('active'); respawnStatus.querySelector('.respawn-status-text').textContent = - `Active (${this.respawnStatus[sessionId].state || 'running'})`; + this.respawnStatus[sessionId].state || 'Active'; + enableBtn.style.display = 'none'; stopBtn.style.display = ''; - configBtn.textContent = 'Reconfigure'; } else { respawnStatus.classList.remove('active'); respawnStatus.querySelector('.respawn-status-text').textContent = 'Not active'; + enableBtn.style.display = ''; stopBtn.style.display = 'none'; - configBtn.textContent = 'Configure Respawn'; } // Only show respawn section for claude mode sessions with a running process @@ -1233,7 +1109,73 @@ class ClaudemanApp { setTimeout(() => document.getElementById('sessionNameInput').focus(), 100); } - async stopRespawnFromOptions() { + // Get respawn config from modal inputs + getModalRespawnConfig() { + const updatePrompt = document.getElementById('modalRespawnPrompt').value; + const idleTimeout = parseInt(document.getElementById('modalRespawnIdleTimeout').value) || 5; + const stepDelay = parseInt(document.getElementById('modalRespawnStepDelay').value) || 1; + const sendClear = document.getElementById('modalRespawnSendClear').checked; + const sendInit = document.getElementById('modalRespawnSendInit').checked; + const durationStr = document.getElementById('modalRespawnDuration').value; + const durationMinutes = durationStr ? parseInt(durationStr) : null; + const autoClearEnabled = document.getElementById('modalAutoClearEnabled').checked; + const autoClearThreshold = parseInt(document.getElementById('modalAutoClearThreshold').value) || 100000; + + return { + respawnConfig: { + updatePrompt, + idleTimeoutMs: idleTimeout * 1000, + interStepDelayMs: stepDelay * 1000, + sendClear, + sendInit, + }, + durationMinutes, + autoClearEnabled, + autoClearThreshold + }; + } + + async enableRespawnFromModal() { + if (!this.editingSessionId) { + this.showToast('No session selected', 'warning'); + return; + } + + const { respawnConfig, durationMinutes, autoClearEnabled, autoClearThreshold } = this.getModalRespawnConfig(); + + try { + // Enable respawn on the session + const res = await fetch(`/api/sessions/${this.editingSessionId}/respawn/enable`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ config: respawnConfig, durationMinutes }) + }); + const data = await res.json(); + if (data.error) throw new Error(data.error); + + // Set auto-clear if enabled + if (autoClearEnabled) { + await fetch(`/api/sessions/${this.editingSessionId}/auto-clear`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: true, threshold: autoClearThreshold }) + }); + } + + // Update UI + const respawnStatus = document.getElementById('sessionRespawnStatus'); + respawnStatus.classList.add('active'); + respawnStatus.querySelector('.respawn-status-text').textContent = 'WATCHING'; + document.getElementById('modalEnableRespawnBtn').style.display = 'none'; + document.getElementById('modalStopRespawnBtn').style.display = ''; + + this.showToast('Respawn enabled', 'success'); + } catch (err) { + this.showToast('Failed to enable respawn: ' + err.message, 'error'); + } + } + + async stopRespawnFromModal() { if (!this.editingSessionId) return; try { await fetch(`/api/sessions/${this.editingSessionId}/respawn/stop`, { method: 'POST' }); @@ -1243,8 +1185,8 @@ class ClaudemanApp { 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'; + document.getElementById('modalEnableRespawnBtn').style.display = ''; + document.getElementById('modalStopRespawnBtn').style.display = 'none'; this.showToast('Respawn stopped', 'success'); } catch (err) { @@ -1412,7 +1354,6 @@ class ClaudemanApp { this.closeSessionOptions(); this.closeAppSettings(); this.cancelCloseSession(); - document.getElementById('respawnPanel').classList.remove('open'); document.getElementById('monitorPanel').classList.remove('open'); } diff --git a/src/web/public/index.html b/src/web/public/index.html index ff05268e..c59476ac 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -25,16 +25,16 @@
-
- - Connecting -
-
0 tokens
14
+
+ + Connecting +
+
0 tokens
@@ -73,7 +73,11 @@ - +
+ + + +
@@ -102,78 +106,6 @@ - -
-
- Respawn Settings - -
-
-
- - -
-
-
- -
- - sec -
-
-
- -
- - sec -
-
-
-
- -
- - min -
- Leave empty for unlimited. Respawn stops after duration. -
-
- -
- - -
-
-
- -
- -
-
- - tokens -
-
-
- - - -
-
-
- - +