diff --git a/src/session.ts b/src/session.ts index 9561b337..38328dd8 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1218,6 +1218,7 @@ export class Session extends EventEmitter { }; // Forward to Ralph tracker to detect Ralph loops and todos + // (opencode sessions already returned early at line 1209) if (this._ralphTracker.enabled || !this._ralphTracker.autoEnableDisabled) { this._ralphTracker.processCleanData(getCleanData()); } diff --git a/src/web/public/app.js b/src/web/public/app.js index 0481fe1b..932ff523 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -4056,6 +4056,11 @@ class ClaudemanApp { this.terminal.scrollToBottom(); // Re-position local echo overlay at new prompt location this._localEchoOverlay?.rerender(); + // Resize PTY to match actual browser dimensions (critical for OpenCode + // TUI sessions that render at fixed 120x40 until told the real size) + if (this.activeSessionId) { + this.sendResize(this.activeSessionId); + } } } catch (err) { console.error('needsRefresh reload failed:', err); @@ -6191,6 +6196,14 @@ class ClaudemanApp { const sessionNameEl = document.getElementById('closeConfirmSessionName'); sessionNameEl.textContent = name; + // Update kill button text based on session mode + const killTitle = document.getElementById('closeConfirmKillTitle'); + if (killTitle) { + killTitle.textContent = session.mode === 'opencode' + ? 'Kill Tmux & OpenCode' + : 'Kill Tmux & Claude Code'; + } + document.getElementById('closeConfirmModal').classList.add('active'); } @@ -9629,9 +9642,9 @@ class ClaudemanApp { const data = await res.json(); if (!data.success) throw new Error(data.error || 'Failed to start OpenCode'); - // Switch to the new session + // Switch to the new session (don't pre-set activeSessionId — selectSession + // early-returns when IDs match, skipping buffer load and sendResize) if (data.sessionId) { - this.activeSessionId = data.sessionId; await this.selectSession(data.sessionId); } @@ -10461,13 +10474,28 @@ class ClaudemanApp { document.getElementById('respawnPresetSelect').value = ''; document.getElementById('presetDescriptionHint').textContent = ''; - // Populate Ralph Wiggum form with current session values - const ralphState = this.ralphStates.get(sessionId); - this.populateRalphForm({ - enabled: ralphState?.loop?.enabled ?? session.ralphLoop?.enabled ?? false, - completionPhrase: ralphState?.loop?.completionPhrase || session.ralphLoop?.completionPhrase || '', - maxIterations: ralphState?.loop?.maxIterations || session.ralphLoop?.maxIterations || 0, - }); + // Hide Ralph/Todo tab and Respawn tab for opencode sessions (not supported) + const ralphTabBtn = document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="ralph"]'); + const respawnTabBtn = document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="respawn"]'); + if (isOpenCode) { + if (ralphTabBtn) ralphTabBtn.style.display = 'none'; + if (respawnTabBtn) respawnTabBtn.style.display = 'none'; + // Default to Context tab for opencode sessions since Respawn is hidden + this.switchOptionsTab('context'); + } else { + if (ralphTabBtn) ralphTabBtn.style.display = ''; + if (respawnTabBtn) respawnTabBtn.style.display = ''; + } + + // Populate Ralph Wiggum form with current session values (skip for opencode) + if (!isOpenCode) { + const ralphState = this.ralphStates.get(sessionId); + this.populateRalphForm({ + enabled: ralphState?.loop?.enabled ?? session.ralphLoop?.enabled ?? false, + completionPhrase: ralphState?.loop?.completionPhrase || session.ralphLoop?.completionPhrase || '', + maxIterations: ralphState?.loop?.maxIterations || session.ralphLoop?.maxIterations || 0, + }); + } const modal = document.getElementById('sessionOptionsModal'); modal.classList.add('active'); diff --git a/src/web/public/index.html b/src/web/public/index.html index 2ddb276d..d9818b26 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -782,10 +782,10 @@
diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index aa8b0925..1eda8bb1 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -560,26 +560,78 @@ html.mobile-init .file-browser-panel { color: #fff; } - /* Primary action button - Run Claude (matches arrow button blue) */ - .btn-toolbar.btn-claude { + /* Run button group — split button with dropdown */ + .run-btn-group { + display: flex; flex: 0 0 auto; - padding: 0 0.75rem !important; - background: #1e3a5f; - border: 1px solid rgba(59, 130, 246, 0.3); - color: #93c5fd; - font-weight: 500; } - .btn-toolbar.btn-claude:hover, - .btn-toolbar.btn-claude:active { + .btn-toolbar.btn-run { + flex: 0 0 auto; + padding: 0 0.6rem !important; + font-weight: 500; + border-radius: 4px 0 0 4px !important; + border-right: none !important; + } + + .btn-toolbar.btn-run svg { + width: 10px; + height: 10px; + margin-right: 3px; + } + + .btn-toolbar.btn-run-gear { + flex: 0 0 auto; + padding: 0 0.35rem !important; + min-width: unset !important; + border-radius: 0 4px 4px 0 !important; + border-left: 1px solid rgba(255,255,255,0.15) !important; + } + + .btn-toolbar.btn-run-gear svg { + width: 10px; + height: 10px; + } + + /* Claude mode colors on mobile */ + .btn-toolbar.btn-run.mode-claude, + .btn-toolbar.btn-run-gear.mode-claude { + background: #1e3a5f; + border-color: rgba(59, 130, 246, 0.3); + color: #93c5fd; + } + + .btn-toolbar.btn-run.mode-claude:active, + .btn-toolbar.btn-run-gear.mode-claude:active { background: #2563eb; border-color: rgba(59, 130, 246, 0.5); } - .btn-toolbar.btn-claude svg { - width: 10px; - height: 10px; - margin-right: 4px; + /* OpenCode mode colors on mobile */ + .btn-toolbar.btn-run.mode-opencode, + .btn-toolbar.btn-run-gear.mode-opencode { + background: #0a2e2a; + border-color: rgba(16, 185, 129, 0.3); + color: #6ee7b7; + } + + .btn-toolbar.btn-run.mode-opencode:active, + .btn-toolbar.btn-run-gear.mode-opencode:active { + background: #0d4a40; + border-color: rgba(16, 185, 129, 0.5); + } + + /* Run mode dropdown menu — positioned above toolbar on mobile */ + .run-mode-menu { + bottom: 100%; + left: 0; + margin-bottom: 6px; + min-width: 140px; + } + + .run-mode-option { + padding: 10px 12px; + font-size: 0.8rem; } /* Stop button - visible on mobile, icon-only */ @@ -617,19 +669,12 @@ html.mobile-init .file-browser-panel { display: none !important; } - /* Simplified toolbar layout - Run Claude, Shell, and Case */ + /* Simplified toolbar layout — Run, Shell, and Case */ .toolbar-left .toolbar-group:first-child { width: 100%; gap: 8px; } - .btn-toolbar.btn-claude { - flex: 0 0 auto; - min-width: fit-content; - white-space: nowrap; - padding: 0 10px !important; - } - .btn-toolbar.btn-shell { flex: 0 0 auto; min-width: fit-content; @@ -643,7 +688,7 @@ html.mobile-init .file-browser-panel { flex: 1 1 0 !important; min-width: 0; padding: 0 8px !important; - order: 2; /* Between Run Claude and Run Shell */ + order: 2; /* Between Run and Run Shell */ overflow: hidden; justify-content: center; gap: 4px; @@ -656,7 +701,7 @@ html.mobile-init .file-browser-panel { white-space: nowrap; } - /* Mobile case settings gear button - next to Run Claude */ + /* Mobile case settings gear button - next to Run */ .btn-case-settings-mobile { display: inline-flex !important; align-items: center; @@ -669,7 +714,7 @@ html.mobile-init .file-browser-panel { color: #9ca3af; cursor: pointer; flex-shrink: 0; - order: 1; /* Next to Run Claude */ + order: 1; /* Next to Run */ } .btn-case-settings-mobile:active { diff --git a/src/web/server.ts b/src/web/server.ts index 301d23fb..16c78cbd 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -1389,6 +1389,11 @@ export class WebServer extends EventEmitter { return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found'); } + // Ralph tracker is not supported for opencode sessions + if (session.mode === 'opencode') { + return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Ralph tracker is not supported for opencode sessions'); + } + // Handle reset first (before other config) if (reset) { if (reset === 'full') { @@ -1668,7 +1673,8 @@ export class WebServer extends EventEmitter { try { // Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user) - if (this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) { + // Ralph tracker is not supported for opencode sessions + if (session.mode !== 'opencode' && this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) { autoConfigureRalph(session, session.workingDir, () => {}); if (!session.ralphTracker.enabled) { session.ralphTracker.enable(); @@ -1881,6 +1887,11 @@ export class WebServer extends EventEmitter { return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found'); } + // Respawn is not supported for opencode sessions + if (session.mode === 'opencode') { + return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Respawn is not supported for opencode sessions'); + } + // Create or get existing controller let controller = this.respawnControllers.get(id); if (!controller) { @@ -2017,7 +2028,8 @@ export class WebServer extends EventEmitter { try { // Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled and not explicitly disabled by user) - if (this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) { + // Ralph tracker is not supported for opencode sessions + if (session.mode !== 'opencode' && this.store.getConfig().ralphEnabled && !session.ralphTracker.autoEnableDisabled) { autoConfigureRalph(session, session.workingDir, () => {}); if (!session.ralphTracker.enabled) { session.ralphTracker.enable(); @@ -4219,8 +4231,10 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; this.runSummaryTrackers.set(session.id, summaryTracker); summaryTracker.recordSessionStarted(session.mode, session.workingDir); - // Set working directory for Ralph tracker to auto-load @fix_plan.md - session.ralphTracker.setWorkingDir(session.workingDir); + // Set working directory for Ralph tracker to auto-load @fix_plan.md (not supported for opencode sessions) + if (session.mode !== 'opencode') { + session.ralphTracker.setWorkingDir(session.workingDir); + } // Start watching for new images in this session's working directory (if enabled globally and per-session) if (await this.isImageWatcherEnabled() && session.imageWatcherEnabled) { @@ -5422,20 +5436,22 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; console.log(`[Server] Restored tokens for session ${session.id}: ${totalTokens} tokens, $${(savedState.totalCost ?? 0).toFixed(4)}`); } } - // Ralph / Todo tracker - if (savedState.ralphAutoEnableDisabled) { - session.ralphTracker.disableAutoEnable(); - console.log(`[Server] Restored Ralph auto-enable disabled for session ${session.id}`); - } else if (savedState.ralphEnabled) { - // If Ralph was enabled and not explicitly disabled, allow re-enabling on restart - session.ralphTracker.enableAutoEnable(); - } - if (savedState.ralphEnabled) { - session.ralphTracker.enable(); - if (savedState.ralphCompletionPhrase) { - session.ralphTracker.startLoop(savedState.ralphCompletionPhrase); + // Ralph / Todo tracker (not supported for opencode sessions) + if (session.mode !== 'opencode') { + if (savedState.ralphAutoEnableDisabled) { + session.ralphTracker.disableAutoEnable(); + console.log(`[Server] Restored Ralph auto-enable disabled for session ${session.id}`); + } else if (savedState.ralphEnabled) { + // If Ralph was enabled and not explicitly disabled, allow re-enabling on restart + session.ralphTracker.enableAutoEnable(); + } + if (savedState.ralphEnabled) { + session.ralphTracker.enable(); + if (savedState.ralphCompletionPhrase) { + session.ralphTracker.startLoop(savedState.ralphCompletionPhrase); + } + console.log(`[Server] Restored Ralph tracker for session ${session.id} (phrase: ${savedState.ralphCompletionPhrase || 'none'})`); } - console.log(`[Server] Restored Ralph tracker for session ${session.id} (phrase: ${savedState.ralphCompletionPhrase || 'none'})`); } // Nice priority config if (savedState.niceEnabled !== undefined) { @@ -5448,8 +5464,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; if (savedState.flickerFilterEnabled !== undefined) { session.flickerFilterEnabled = savedState.flickerFilterEnabled; } - // Respawn controller - if (savedState.respawnEnabled && savedState.respawnConfig) { + // Respawn controller (not supported for opencode sessions) + if (session.mode !== 'opencode' && savedState.respawnEnabled && savedState.respawnConfig) { try { this.restoreRespawnController(session, savedState.respawnConfig, 'state.json'); } catch (err) { @@ -5458,8 +5474,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; } } - // Fallback: restore respawn from mux-sessions.json if state.json didn't have it - if (!this.respawnControllers.has(session.id) && muxSession.respawnConfig?.enabled) { + // Fallback: restore respawn from mux-sessions.json if state.json didn't have it (not supported for opencode) + if (session.mode !== 'opencode' && !this.respawnControllers.has(session.id) && muxSession.respawnConfig?.enabled) { try { this.restoreRespawnController(session, muxSession.respawnConfig, 'mux-sessions.json'); } catch (err) { @@ -5468,7 +5484,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; } // Fallback: restore Ralph state from state-inner.json if not already set and not explicitly disabled - if (!session.ralphTracker.enabled && !session.ralphTracker.autoEnableDisabled) { + // Ralph tracker is not supported for opencode sessions + if (session.mode !== 'opencode' && !session.ralphTracker.enabled && !session.ralphTracker.autoEnableDisabled) { const ralphState = this.store.getRalphState(muxSession.sessionId); if (ralphState?.loop?.enabled) { session.ralphTracker.restoreState(ralphState.loop, ralphState.todos); @@ -5476,8 +5493,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; } } - // Fallback: auto-detect completion phrase from CLAUDE.md - if (session.ralphTracker.enabled && !session.ralphTracker.loopState.completionPhrase) { + // Fallback: auto-detect completion phrase from CLAUDE.md (not supported for opencode) + if (session.mode !== 'opencode' && session.ralphTracker.enabled && !session.ralphTracker.loopState.completionPhrase) { const claudeMdPath = join(session.workingDir, 'CLAUDE.md'); const completionPhrase = extractCompletionPhrase(claudeMdPath); if (completionPhrase) {