diff --git a/src/web/public/app.js b/src/web/public/app.js index 7ab13426..fda0564e 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -13,7 +13,7 @@ class ClaudemanApp { this.fitAddon = null; this.activeSessionId = null; // Currently active interactive session this.respawnStatus = {}; // Respawn status per session - this.respawnEnabled = true; // Whether respawn is enabled for new sessions + this.respawnEnabled = false; // Whether respawn is enabled for new sessions (disabled by default) this.init(); } @@ -23,6 +23,7 @@ class ClaudemanApp { this.connectSSE(); this.loadState(); this.loadCases(); + this.loadQuickStartCases(); this.startTimerUpdates(); this.setupEventListeners(); @@ -123,6 +124,11 @@ class ClaudemanApp { const customGroup = document.getElementById('customDurationGroup'); customGroup.style.display = e.target.value === 'custom' ? 'block' : 'none'; }); + + // Quick Start case select + document.getElementById('quickStartCase').addEventListener('change', (e) => { + this.handleQuickStartSelect(e.target.value); + }); } // Settings @@ -271,6 +277,7 @@ class ClaudemanApp { this.eventSource.addEventListener('case:created', (e) => { this.loadCases(); + this.loadQuickStartCases(); }); // Respawn events @@ -402,6 +409,120 @@ class ClaudemanApp { } } + // Quick Start + async loadQuickStartCases() { + try { + const res = await fetch('/api/cases'); + const cases = await res.json(); + const select = document.getElementById('quickStartCase'); + + // Preserve current selection + const currentValue = select.value; + + // Clear and rebuild options + select.innerHTML = ''; + + // Add existing cases (skip testcase if it exists) + cases.forEach(c => { + if (c.name !== 'testcase') { + const option = document.createElement('option'); + option.value = c.name; + option.textContent = c.name; + select.appendChild(option); + } + }); + + // Add "Create new..." option + const createOption = document.createElement('option'); + createOption.value = '__create_new__'; + createOption.textContent = '+ Create new...'; + select.appendChild(createOption); + + // Restore selection if it still exists + if ([...select.options].some(o => o.value === currentValue)) { + select.value = currentValue; + } + } catch (err) { + console.error('Failed to load Quick Start cases:', err); + } + } + + handleQuickStartSelect(value) { + if (value === '__create_new__') { + const newName = prompt('Enter a name for the new case:', ''); + const select = document.getElementById('quickStartCase'); + + if (newName && newName.trim()) { + const cleanName = newName.trim().replace(/[^a-zA-Z0-9_-]/g, '-'); + if (cleanName) { + // Add the new option and select it + const option = document.createElement('option'); + option.value = cleanName; + option.textContent = cleanName; + // Insert before the "Create new..." option + select.insertBefore(option, select.lastElementChild); + select.value = cleanName; + } else { + select.value = 'testcase'; + } + } else { + // Revert to default + select.value = 'testcase'; + } + } + } + + async quickStart() { + const select = document.getElementById('quickStartCase'); + let caseName = select.value; + + if (caseName === '__create_new__') { + caseName = 'testcase'; + } + + this.terminal.clear(); + this.terminal.writeln('\x1b[1;32m⚡ Quick Start: Launching Claude session...\x1b[0m'); + this.terminal.writeln(`\x1b[90mCase: ${caseName}\x1b[0m`); + this.terminal.writeln(''); + + try { + const res = await fetch('/api/quick-start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ caseName }) + }); + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error); + } + + this.activeSessionId = data.sessionId; + this.terminal.writeln(`\x1b[1;32m✓ Session started in ${data.casePath}\x1b[0m`); + this.terminal.writeln(''); + + // Reload cases in case a new one was created + this.loadQuickStartCases(); + this.loadCases(); + + // Send initial resize + const dims = this.fitAddon.proposeDimensions(); + if (dims) { + await fetch(`/api/sessions/${data.sessionId}/resize`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) + }); + } + + // Focus the terminal + this.terminal.focus(); + + } catch (err) { + this.terminal.writeln(`\x1b[1;31m❌ Error: ${err.message}\x1b[0m`); + } + } + renderCases() { const list = document.getElementById('casesList'); diff --git a/src/web/public/index.html b/src/web/public/index.html index 415a22e7..2cbecccb 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -77,6 +77,28 @@
+ +
+
+ ⚡ + Quick Start +
+
+ + +
+

Start an interactive Claude session in one click

+
+ +
+ or configure a custom run +
+
@@ -116,7 +138,7 @@
diff --git a/src/web/public/styles.css b/src/web/public/styles.css index e887c93a..7e3a9891 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -885,3 +885,98 @@ input[type="checkbox"] { padding: 0.2rem 0.4rem; font-size: 0.65rem; } + +/* Quick Start Section */ +.quick-start-section { + background: linear-gradient(135deg, #0d4020 0%, #0a2510 100%); + border: 1px solid var(--green); + border-radius: var(--radius); + padding: 1rem; + margin-bottom: 1rem; +} + +.quick-start-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.quick-start-icon { + font-size: 1.25rem; + color: var(--green); +} + +.quick-start-title { + font-size: 1rem; + font-weight: 600; + color: var(--green); +} + +.quick-start-controls { + display: flex; + gap: 0.5rem; + align-items: stretch; +} + +.quick-start-select { + flex: 1; + padding: 0.625rem 0.75rem; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: var(--radius); + color: var(--text); + font-size: 0.875rem; +} + +.quick-start-select:focus { + outline: none; + border-color: var(--green); +} + +.btn-quick-start { + background: var(--green); + color: #000; + font-weight: 600; + padding: 0.625rem 1.25rem; + border: none; + border-radius: var(--radius); + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.btn-quick-start:hover { + background: #16a34a; + transform: translateY(-1px); +} + +.btn-quick-start:active { + transform: translateY(0); +} + +.quick-start-hint { + font-size: 0.7rem; + color: rgba(34, 197, 94, 0.7); + margin-top: 0.5rem; +} + +/* Section Divider */ +.section-divider { + display: flex; + align-items: center; + gap: 1rem; + margin: 1.25rem 0; + color: var(--text-muted); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.section-divider::before, +.section-divider::after { + content: ''; + flex: 1; + height: 1px; + background: var(--border); +} diff --git a/src/web/server.ts b/src/web/server.ts index 8af159d3..6769b5a5 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -442,6 +442,54 @@ export class WebServer extends EventEmitter { hasClaudeMd: existsSync(join(casePath, 'CLAUDE.md')), }; }); + + // Quick Start: Create case (if needed) and start interactive session in one click + this.app.post('/api/quick-start', async (req) => { + const { caseName = 'testcase' } = req.body as { caseName?: string }; + + // Validate case name + if (!/^[a-zA-Z0-9_-]+$/.test(caseName)) { + return { success: false, error: 'Invalid case name. Use only letters, numbers, hyphens, underscores.' }; + } + + const casePath = join(casesDir, caseName); + + // Create case folder and CLAUDE.md if it doesn't exist + if (!existsSync(casePath)) { + try { + mkdirSync(casePath, { recursive: true }); + mkdirSync(join(casePath, 'src'), { recursive: true }); + + const claudeMd = generateClaudeMd(caseName, ''); + writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd); + + this.broadcast('case:created', { name: caseName, path: casePath }); + } catch (err) { + return { success: false, error: `Failed to create case: ${(err as Error).message}` }; + } + } + + // Create a new session with the case as working directory + const session = new Session({ workingDir: casePath }); + this.sessions.set(session.id, session); + this.setupSessionListeners(session); + this.broadcast('session:created', session.toDetailedState()); + + // Start interactive mode + try { + await session.startInteractive(); + this.broadcast('session:interactive', { id: session.id }); + + return { + success: true, + sessionId: session.id, + casePath, + caseName, + }; + } catch (err) { + return { success: false, error: (err as Error).message }; + } + }); } private setupSessionListeners(session: Session): void {