From 02f3bea3c105c6f587622c346009644d6a0a3816 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 26 Feb 2026 10:16:20 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20OpenCode=20integration=20Phase=205=20?= =?UTF-8?q?=E2=80=94=20API=20routes,=20frontend=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API changes: - GET /api/opencode/status — check if OpenCode CLI is available - POST /api/sessions — support mode: 'opencode' with openCodeConfig - POST /api/quick-start — support mode: 'opencode', guard writeHooksConfig - Fix hardcoded mode: 'claude' in lifecycle logs (use session.mode) Schema changes: - Add 'opencode' to mode enums in CreateSessionSchema, QuickStartSchema - Add OpenCodeConfigSchema (model, autoAllowTools, continueSession, etc.) - Add 'OPENCODE_' to ALLOWED_ENV_PREFIXES - Block OPENCODE_SERVER_PASSWORD in BLOCKED_ENV_KEYS (atomic with prefix addition) Frontend UI: - "Run OC" button in toolbar + "Run OpenCode" on welcome screen - Green "oc" tab badge for OpenCode sessions (matching green brand) - runOpenCode() method — checks availability, quick-starts with autoAllowTools - Feature gating: hide Respawn/Context/Ralph tabs for OpenCode sessions - Session options defaults to Summary tab for OpenCode - CSS: OpenCode button styles, tab badge styles Co-Authored-By: Claude Opus 4.6 (1M context) --- src/web/public/app.js | 53 +++++++++++++++++++++++++++++++++++--- src/web/public/index.html | 13 +++++++--- src/web/public/styles.css | 32 +++++++++++++++++++++++ src/web/schemas.ts | 20 ++++++++++++--- src/web/server.ts | 54 ++++++++++++++++++++++++++++++++------- 5 files changed, 153 insertions(+), 19 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 195a4c43..8e846c18 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -5440,7 +5440,7 @@ class ClaudemanApp { - ${mode === 'shell' ? '' : ''} + ${mode === 'shell' ? '' : mode === 'opencode' ? '' : ''} ${this.escapeHtml(name)} ${showFolder ? `\u{1F4C1} ${this.escapeHtml(folderName)}` : ''} @@ -9507,6 +9507,48 @@ class ClaudemanApp { } } + async runOpenCode() { + const caseName = document.getElementById('quickStartCase').value || 'testcase'; + + this.terminal.clear(); + this.terminal.writeln(`\x1b[1;32m Starting OpenCode session in ${caseName}...\x1b[0m`); + this.terminal.writeln(''); + + try { + // Check if OpenCode is available + const statusRes = await fetch('/api/opencode/status'); + const status = await statusRes.json(); + if (!status.available) { + this.terminal.writeln('\x1b[1;31m OpenCode CLI not found.\x1b[0m'); + this.terminal.writeln('\x1b[90m Install with: curl -fsSL https://opencode.ai/install | bash\x1b[0m'); + return; + } + + // Quick-start with opencode mode (auto-allow tools by default) + const res = await fetch('/api/quick-start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + caseName, + mode: 'opencode', + openCodeConfig: { autoAllowTools: true }, + }) + }); + const data = await res.json(); + if (!data.success) throw new Error(data.error || 'Failed to start OpenCode'); + + // Switch to the new session + if (data.sessionId) { + this.activeSessionId = data.sessionId; + await this.selectSession(data.sessionId); + } + + this.terminal.focus(); + } catch (err) { + this.terminal.writeln(`\x1b[1;31m Error: ${err.message}\x1b[0m`); + } + } + // ========== Directory Input ========== toggleDirInput() { @@ -10263,8 +10305,8 @@ class ClaudemanApp { this.editingSessionId = sessionId; - // Reset to Respawn tab - this.switchOptionsTab('respawn'); + // Reset to an appropriate tab — Summary for OpenCode (Respawn/Ralph are Claude-only) + this.switchOptionsTab(session.mode === 'opencode' ? 'summary' : 'respawn'); // Update respawn status display and buttons const respawnStatus = document.getElementById('sessionRespawnStatus'); @@ -10292,6 +10334,11 @@ class ClaudemanApp { respawnSection.style.display = 'none'; } + // Hide Claude-specific options for OpenCode sessions + const isOpenCode = session.mode === 'opencode'; + const claudeOnlyEls = document.querySelectorAll('[data-claude-only]'); + claudeOnlyEls.forEach(el => { el.style.display = isOpenCode ? 'none' : ''; }); + // Reset duration presets to default (unlimited) this.selectDurationPreset(''); diff --git a/src/web/public/index.html b/src/web/public/index.html index f8cffcb6..50e56dca 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -238,6 +238,10 @@ Run Claude + +