fix: address PR review findings for Gemini run mode + Ralph todo-config

Gemini (PR #134) blockers:
- runGemini() now unwraps the {success,data} envelope: status check reads
  .data.available, quick-start reads data.data.sessionId (was reading the raw
  shape, so the Run-Gemini button could never start a session).
- setGeminiEnvVars() now uses the socket-scoped ${this.tmux()} setenv instead of
  bare tmux — Gemini/Google auth env vars were targeting the wrong tmux server
  and silently failing on every install.

Gemini parity polish:
- gemini tab-mode badge ('gm') + .tab-mode.gemini CSS; kill-dialog label
  'Kill Tmux & Gemini'; codeman doctor dependency-registry entry; export
  isGeminiAvailable from utils barrel; COLORTERM=truecolor + unset NO_COLOR;
  add gemini to isAltScreenStripMode (Ink TUI, repaints inline like Codex/Claude).
- Revert 4 system-routes.test.ts envelope assertions weakened to
  (body.message ?? body.error) back to (body.success === false).
- Add a runGemini() vm-sandbox test that drives the envelope path end-to-end.

Ralph todo-config (PR #135): maxTodos/todoExpirationMinutes are now persisted
and read back — surfaced via the loopState getter (RalphTrackerState) into
toState()/SSE broadcast and restored in restoreState(), mirroring maxIterations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-06-25 00:26:06 +02:00
parent 9cc310e843
commit 368fc20fc2
11 changed files with 106 additions and 21 deletions
+8
View File
@@ -90,6 +90,14 @@ export const DEPENDENCY_REGISTRY: ToolDependency[] = [
usedBy: ['Codex sessions'],
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['codex'], versionArg: '--version' } }],
},
{
id: 'gemini',
label: 'Gemini CLI',
category: 'core',
required: false,
usedBy: ['Gemini sessions'],
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['gemini'], versionArg: '--version' } }],
},
{
id: 'libreoffice',
label: 'LibreOffice',
+10
View File
@@ -1059,6 +1059,10 @@ export class RalphTracker extends EventEmitter {
planVersion: this.planTracker.planVersion,
planHistoryLength: this.planTracker.getPlanHistory().length,
completionConfidence: this._lastCompletionConfidence,
// Surface the live todo-config so it persists (toState) and reads back into
// the Session Options modal (broadcast) — mirrors maxIterations round-trip.
maxTodos: this._maxTodos,
todoExpirationMinutes: this.todoExpirationMinutes,
};
}
@@ -2345,6 +2349,12 @@ export class RalphTracker extends EventEmitter {
...loopState,
enabled: loopState.enabled ?? false,
};
// Restore the per-session todo-config into the live fields used by the hot
// paths (eviction cap + expiry). Setters ignore non-positive values.
if (typeof loopState.maxTodos === 'number') this.setMaxTodos(loopState.maxTodos);
if (typeof loopState.todoExpirationMinutes === 'number') {
this.setTodoExpirationMinutes(loopState.todoExpirationMinutes);
}
this._todos.clear();
for (const todo of todos) {
this._todos.set(todo.id, {
+7 -6
View File
@@ -157,14 +157,15 @@ function getModeLabel(mode: SessionMode): string {
* that we strip so the browser keeps everything in the main buffer with scrollback
* reachable (the strip runs on both the live stream and the buffer replay).
*
* Codex and Claude Code are known, controlled TUIs that repaint via cursor
* positioning, so dropping the alt-screen switch is safe — content stays in the
* normal buffer. Excluded: `shell` (arbitrary programs like vim/less/htop
* legitimately need the alt screen) and `opencode` (renders its own TUI that
* may rely on it). Keep parity with the replay-side strip in session-routes.ts.
* Codex, Claude Code, and Gemini are known, controlled (Ink/React) TUIs that
* repaint via cursor positioning, so dropping the alt-screen switch is safe —
* content stays in the normal buffer. Excluded: `shell` (arbitrary programs like
* vim/less/htop legitimately need the alt screen) and `opencode` (renders its own
* TUI that may rely on it). Keep parity with the replay-side strip in
* session-routes.ts.
*/
export function isAltScreenStripMode(mode: SessionMode): boolean {
return mode === 'codex' || mode === 'claude';
return mode === 'codex' || mode === 'claude' || mode === 'gemini';
}
// Note: Claude CLI PATH resolution moved to session-cli-builder.ts (buildClaudeEnv)
+5 -5
View File
@@ -720,7 +720,7 @@ function setCodexEnvVars(tmuxCmd: string, muxName: string): void {
* Gemini Pro/Ultra users usually authenticate via cached Google login; these
* variables cover API-key and Vertex AI paths without putting secrets in ps.
*/
function setGeminiEnvVars(muxName: string): void {
function setGeminiEnvVars(tmuxCmd: string, muxName: string): void {
const sensitiveVars = [
'GEMINI_API_KEY',
'GEMINI_MODEL',
@@ -735,7 +735,7 @@ function setGeminiEnvVars(muxName: string): void {
if (val) {
const escaped = val.replace(/'/g, "'\\''");
try {
execSync(`tmux setenv -t '${muxName}' ${key} '${escaped}'`, {
execSync(`${tmuxCmd} setenv -t '${muxName}' ${key} '${escaped}'`, {
encoding: 'utf8',
timeout: EXEC_TIMEOUT_MS,
stdio: ['pipe', 'pipe', 'pipe'],
@@ -929,8 +929,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
const exports = [
'export LANG=en_US.UTF-8',
'export LC_ALL=en_US.UTF-8',
mode === 'codex' ? 'export COLORTERM=truecolor' : 'unset COLORTERM',
...(mode === 'codex' ? ['unset NO_COLOR'] : []),
mode === 'codex' || mode === 'gemini' ? 'export COLORTERM=truecolor' : 'unset COLORTERM',
...(mode === 'codex' || mode === 'gemini' ? ['unset NO_COLOR'] : []),
'export CODEMAN_MUX=1',
`export CODEMAN_SESSION_ID=${sessionId}`,
`export CODEMAN_MUX_NAME=${muxName}`,
@@ -1034,7 +1034,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
* Configure Gemini-specific environment on a tmux session.
*/
private _configureGemini(muxName: string): void {
setGeminiEnvVars(muxName);
setGeminiEnvVars(this.tmux(), muxName);
}
/**
+4
View File
@@ -90,6 +90,10 @@ export interface RalphTrackerState {
cycleCount: number;
/** Maximum iterations if detected */
maxIterations: number | null;
/** Max todos retained for this session before FIFO eviction (persisted; default = global cap) */
maxTodos?: number;
/** Todo auto-expiry in minutes (persisted; default = global TODO_EXPIRY_MS) */
todoExpirationMinutes?: number;
/** Timestamp of last activity */
lastActivity: number;
/** Elapsed hours if detected */
+1 -1
View File
@@ -29,4 +29,4 @@ export { wrapWithNice } from './nice-wrapper.js';
export { findClaudeDir, getAugmentedPath } from './claude-cli-resolver.js';
export { resolveOpenCodeDir } from './opencode-cli-resolver.js';
export { resolveCodexDir, isCodexAvailable } from './codex-cli-resolver.js';
export { resolveGeminiDir } from './gemini-cli-resolver.js';
export { resolveGeminiDir, isGeminiAvailable } from './gemini-cli-resolver.js';
+4 -2
View File
@@ -3004,7 +3004,7 @@ class CodemanApp {
<span class="tab-status ${status}" aria-hidden="true"></span>
<span class="tab-info">
<span class="tab-name-row">
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : mode === 'opencode' ? '<span class="tab-mode opencode" aria-hidden="true">oc</span>' : mode === 'codex' ? '<span class="tab-mode codex" aria-hidden="true">cx</span>' : ''}
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : mode === 'opencode' ? '<span class="tab-mode opencode" aria-hidden="true">oc</span>' : mode === 'codex' ? '<span class="tab-mode codex" aria-hidden="true">cx</span>' : mode === 'gemini' ? '<span class="tab-mode gemini" aria-hidden="true">gm</span>' : ''}
<span class="tab-name" data-session-id="${id}">${(() => { const p = parseSessionPrefix(name); return p && p.suffix ? '<span class="tab-prefix">' + escapeHtml(p.prefix) + '</span><span class="tab-suffix">: ' + escapeHtml(p.suffix) + '</span>' : escapeHtml(name); })()}</span>
<span class="tab-detached-badge" aria-hidden="true">detached</span>
</span>
@@ -3988,7 +3988,9 @@ class CodemanApp {
? 'Kill Tmux & OpenCode'
: session.mode === 'codex'
? 'Kill Tmux & Codex'
: 'Kill Tmux & Claude Code';
: session.mode === 'gemini'
? 'Kill Tmux & Gemini'
: 'Kill Tmux & Claude Code';
}
document.getElementById('closeConfirmModal').classList.add('active');
+5 -3
View File
@@ -653,7 +653,7 @@ Object.assign(CodemanApp.prototype, {
try {
const statusRes = await fetch('/api/gemini/status');
const status = await statusRes.json();
const status = (await statusRes.json()).data;
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');
@@ -674,8 +674,8 @@ Object.assign(CodemanApp.prototype, {
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);
if (data.data.sessionId) {
await this.selectSession(data.data.sessionId);
}
this.terminal.focus();
@@ -796,6 +796,8 @@ Object.assign(CodemanApp.prototype, {
enabled: ralphState?.loop?.enabled ?? session.ralphLoop?.enabled ?? false,
completionPhrase: ralphState?.loop?.completionPhrase || session.ralphLoop?.completionPhrase || '',
maxIterations: ralphState?.loop?.maxIterations || session.ralphLoop?.maxIterations || 0,
maxTodos: ralphState?.loop?.maxTodos || session.ralphLoop?.maxTodos,
todoExpirationMinutes: ralphState?.loop?.todoExpirationMinutes || session.ralphLoop?.todoExpirationMinutes,
});
}
+5
View File
@@ -1178,6 +1178,11 @@ body.solo-mode .btn-lifecycle-log {
color: #a855f7;
}
.session-tab .tab-mode.gemini {
background: rgba(138, 180, 248, 0.2);
color: #8ab4f8;
}
/* Timer Banner - Compact */
.timer-banner {
display: flex;