Merge PR #134: Gemini run mode (third external-CLI mode alongside Codex/OpenCode) (COD-36)

This commit is contained in:
Codeman maintainer
2026-06-25 00:10:52 +02:00
17 changed files with 520 additions and 40 deletions
+7
View File
@@ -306,6 +306,10 @@
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Run OpenCode
</button>
<button class="welcome-btn welcome-btn-gemini" onclick="app.setRunMode('gemini'); app.runGemini()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Run Gemini
</button>
</div>
<div class="welcome-qr" id="welcomeQr" onclick="app.toggleWelcomeQrSize()">
<div class="welcome-qr-inner" id="welcomeQrInner"></div>
@@ -440,6 +444,9 @@
<button class="run-mode-option" data-mode="codex" onclick="app.setRunMode('codex')">
<span class="run-mode-dot codex"></span>Codex
</button>
<button class="run-mode-option" data-mode="gemini" onclick="app.setRunMode('gemini')">
<span class="run-mode-dot gemini"></span>Gemini
</button>
<div class="run-mode-sep"></div>
<div class="run-mode-header">Recent Sessions</div>
<div class="run-mode-history" id="runModeHistory"></div>
+14
View File
@@ -780,6 +780,20 @@ html.mobile-init .file-browser-panel {
border-color: rgba(16, 185, 129, 0.5);
}
/* Gemini mode colors on mobile */
.btn-toolbar.btn-run.mode-gemini,
.btn-toolbar.btn-run-gear.mode-gemini {
background: #10243f;
border-color: rgba(96, 165, 250, 0.3);
color: #dbeafe;
}
.btn-toolbar.btn-run.mode-gemini:active,
.btn-toolbar.btn-run-gear.mode-gemini:active {
background: #174ea6;
border-color: rgba(96, 165, 250, 0.5);
}
/* Run mode dropdown menu — positioned above toolbar on mobile */
.run-mode-menu {
bottom: 100%;
+60 -14
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Quick start (case loading, session spawning for Claude/Shell/OpenCode),
* @fileoverview Quick start (case loading, session spawning for Claude/Shell/OpenCode/Codex/Gemini),
* session options modal (per-session settings, color picker, rename),
* session options tabs (Ralph config tab), case settings (CRUD, links),
* create case modal, and mobile case picker.
@@ -151,7 +151,7 @@ Object.assign(CodemanApp.prototype, {
return this.run();
},
/** Run using the selected mode (Claude Code, OpenCode, or Codex) */
/** Run using the selected mode (Claude Code, OpenCode, Codex, or Gemini) */
async run() {
const mode = this._runMode || 'claude';
if (mode === 'opencode') {
@@ -160,6 +160,9 @@ Object.assign(CodemanApp.prototype, {
if (mode === 'codex') {
return this.runCodex();
}
if (mode === 'gemini') {
return this.runGemini();
}
return this.runClaude();
},
@@ -257,7 +260,7 @@ Object.assign(CodemanApp.prototype, {
gearBtn.className = `btn-toolbar btn-run-gear mode-${mode}`;
}
if (label) {
label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : 'Run';
label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : mode === 'gemini' ? 'Run GM' : 'Run';
}
},
@@ -640,6 +643,47 @@ Object.assign(CodemanApp.prototype, {
}
},
async runGemini() {
const caseName = document.getElementById('quickStartCase').value || 'testcase';
this.terminal.clear();
this.terminal.writeln(`\x1b[1;32m Starting Gemini session in ${caseName}...\x1b[0m`);
this.terminal.writeln('');
this.terminal.focus();
try {
const statusRes = await fetch('/api/gemini/status');
const status = await statusRes.json();
if (!status.available) {
this.terminal.writeln('\x1b[1;31m Gemini CLI not found.\x1b[0m');
this.terminal.writeln('\x1b[90m Install with: npm install -g @google/gemini-cli\x1b[0m');
return;
}
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), this.loadAppSettingsFromStorage());
const res = await fetch('/api/quick-start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
caseName,
mode: 'gemini',
geminiConfig: { approvalMode: 'yolo' },
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
})
});
const data = await res.json();
if (!data.success) throw new Error(data.error || 'Failed to start Gemini');
if (data.sessionId) {
await this.selectSession(data.sessionId);
}
this.terminal.focus();
} catch (err) {
this.terminal.writeln(`\x1b[1;31m Error: ${err.message}\x1b[0m`);
}
},
// ═══════════════════════════════════════════════════════════════
// Session Options Modal
@@ -651,8 +695,9 @@ Object.assign(CodemanApp.prototype, {
this.editingSessionId = sessionId;
// Reset to an appropriate tab — Summary for OpenCode (Respawn/Ralph are Claude-only)
this.switchOptionsTab(session.mode === 'opencode' || session.mode === 'codex' ? 'summary' : 'respawn');
// Reset to an appropriate tab — Summary for external CLIs (Respawn/Ralph are Claude-only)
const isAltMode = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini';
this.switchOptionsTab(isAltMode ? 'summary' : 'respawn');
// Update respawn status display and buttons
const respawnStatus = document.getElementById('sessionRespawnStatus');
@@ -680,10 +725,10 @@ Object.assign(CodemanApp.prototype, {
respawnSection.style.display = 'none';
}
// Hide Claude-specific options for OpenCode sessions
const isOpenCode = session.mode === 'opencode' || session.mode === 'codex';
// Hide Claude-specific options for external CLI sessions
const isExternalCli = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini';
const claudeOnlyEls = document.querySelectorAll('[data-claude-only]');
claudeOnlyEls.forEach(el => { el.style.display = isOpenCode ? 'none' : ''; });
claudeOnlyEls.forEach(el => { el.style.display = isExternalCli ? 'none' : ''; });
// Reset duration presets to default (unlimited)
this.selectDurationPreset('');
@@ -731,21 +776,21 @@ Object.assign(CodemanApp.prototype, {
document.getElementById('respawnPresetSelect').value = '';
document.getElementById('presetDescriptionHint').textContent = '';
// Hide Ralph/Todo tab and Respawn tab for opencode sessions (not supported)
// Hide Ralph/Todo tab and Respawn tab for external CLI 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 (isExternalCli) {
if (ralphTabBtn) ralphTabBtn.style.display = 'none';
if (respawnTabBtn) respawnTabBtn.style.display = 'none';
// Default to Context tab for opencode sessions since Respawn is hidden
// Default to Context tab for external CLI 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) {
// Populate Ralph Wiggum form with current session values (skip for external CLI sessions)
if (!isExternalCli) {
const ralphState = this.ralphStates.get(sessionId);
this.populateRalphForm({
enabled: ralphState?.loop?.enabled ?? session.ralphLoop?.enabled ?? false,
@@ -1579,6 +1624,7 @@ Object.defineProperty(CodemanApp.prototype, 'runMode', {
return this._runMode || 'claude';
},
set(mode) {
this._runMode = mode === 'opencode' || mode === 'codex' || mode === 'claude' ? mode : 'claude';
this._runMode =
mode === 'opencode' || mode === 'codex' || mode === 'gemini' || mode === 'claude' ? mode : 'claude';
},
});
+32
View File
@@ -2362,6 +2362,21 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
transform: translateY(-1px);
}
.welcome-btn-gemini {
background: linear-gradient(135deg, #10243f 0%, #174ea6 55%, #4f46e5 100%);
border-color: rgba(96, 165, 250, 0.4);
color: #dbeafe;
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.welcome-btn-gemini:hover {
background: linear-gradient(135deg, #17345f 0%, #2563eb 55%, #6366f1 100%);
box-shadow: 0 4px 20px rgba(96, 165, 250, 0.3), 0 0 40px rgba(99, 102, 241, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
border-color: rgba(147, 197, 253, 0.5);
color: #eff6ff;
transform: translateY(-1px);
}
.welcome-btn-tunnel {
background: linear-gradient(135deg, #221538 0%, #3b1a7a 50%, #6d28d9 100%);
border-color: rgba(124, 58, 237, 0.4);
@@ -3168,6 +3183,22 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
color: #e9d5ff;
}
/* Gemini mode colors */
.btn-toolbar.btn-run.mode-gemini,
.btn-toolbar.btn-run-gear.mode-gemini {
background: linear-gradient(135deg, #10243f 0%, #174ea6 55%, #4f46e5 100%);
border-color: rgba(96, 165, 250, 0.5);
color: #dbeafe;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.btn-toolbar.btn-run.mode-gemini:hover,
.btn-toolbar.btn-run-gear.mode-gemini:hover {
background: linear-gradient(135deg, #17345f 0%, #2563eb 55%, #6366f1 100%);
box-shadow: 0 0 12px rgba(96, 165, 250, 0.35), 0 2px 8px rgba(99, 102, 241, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08);
border-color: rgba(147, 197, 253, 0.6);
color: #eff6ff;
}
/* Dropdown menu */
.run-mode-menu {
display: none;
@@ -3221,6 +3252,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
.run-mode-dot.claude { background: #3b82f6; }
.run-mode-dot.opencode { background: #10b981; }
.run-mode-dot.codex { background: #a855f7; }
.run-mode-dot.gemini { background: #8ab4f8; }
.run-mode-sep {
height: 1px;