From 3e1a0e679f0ada8a0cf58437c89460f6a0898dba Mon Sep 17 00:00:00 2001 From: timkjr Date: Wed, 26 Aug 2026 21:09:01 -0500 Subject: [PATCH] fix(omp): resume by mode, not silently as claude, and support --continue resumeHistorySession() never sent mode when recreating a session from a history/session-manager row, so the server default silently opened a plain Claude session for every non-claude row -- reproduced live: OMP rows spawned Claude sessions on click. Thread the row's mode through every call site (welcome list, session manager, mobile overview) and only send the Claude-specific resumeSessionId for claude rows. Codeman has no live PTY-reattach outside server boot, and it's moot for OMP anyway (exiting it kills the pane's only process), so route the non-claude relaunch through each CLI's own continue-most-recent flag instead of a context-free fresh start. OMP never got one: buildOmpCommand only implemented --model/--resume despite omp --help documenting -c/--continue. Added continueSession to OmpConfig end-to-end (type, schema, builder) mirroring the existing opencode/pi/grok/deepseek fields, and wired resumeHistorySession to use it. Verified live: told a real omp session a secret, exited it, closed the tab without killing tmux, relaunched with --continue in the same directory, and had it recall the secret. --- src/tmux-manager.ts | 11 ++++++++--- src/types/session.ts | 2 ++ src/web/public/mobile-overview.js | 2 +- src/web/public/panels-ui.js | 2 +- src/web/public/session-ui.js | 2 +- src/web/public/terminal-ui.js | 33 +++++++++++++++++++++++++++---- src/web/schemas.ts | 1 + test/command-palette-ui.test.ts | 2 +- test/omp-mode.test.ts | 20 +++++++++++++++++++ 9 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index e81ee04b..2bd31f96 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -914,9 +914,14 @@ function buildOmpCommand(config?: OmpConfig): string { if (safeModel) parts.push('--model', safeModel); } - if (config?.resumeSessionId) { - const safeId = /^[a-zA-Z0-9._-]+$/.test(config.resumeSessionId) ? config.resumeSessionId : undefined; - if (safeId) parts.push('--resume', safeId); + // --resume and --continue conflict; a valid explicit session id wins, + // mirroring the sibling builders (grok/pi/opencode). + const safeId = + config?.resumeSessionId && /^[a-zA-Z0-9._-]+$/.test(config.resumeSessionId) ? config.resumeSessionId : undefined; + if (safeId) { + parts.push('--resume', safeId); + } else if (config?.continueSession) { + parts.push('--continue'); } return parts.join(' '); diff --git a/src/types/session.ts b/src/types/session.ts index 5c26dc6a..3c34b49d 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -350,6 +350,8 @@ export interface OmpConfig { model?: string; /** Resume a previous conversation (passed via --resume). */ resumeSessionId?: string; + /** Continue the most recent session in this directory (passed via --continue). */ + continueSession?: boolean; } /** diff --git a/src/web/public/mobile-overview.js b/src/web/public/mobile-overview.js index 34ce8963..df647ae9 100644 --- a/src/web/public/mobile-overview.js +++ b/src/web/public/mobile-overview.js @@ -389,7 +389,7 @@ Object.assign(CodemanApp.prototype, { async resumeMobileOverviewSession(sessionId) { const row = (this._mobileOverviewPastRows || []).find((r) => r.id === sessionId); if (!row || !row.workingDir) return; - await this.resumeHistorySession(row.claudeSessionId || row.id, row.workingDir, row.name || undefined); + await this.resumeHistorySession(row.claudeSessionId || row.id, row.workingDir, row.name || undefined, row.mode); }, // ═══════════════════════════════════════════════════════════════ diff --git a/src/web/public/panels-ui.js b/src/web/public/panels-ui.js index f4dcc16f..66197c27 100644 --- a/src/web/public/panels-ui.js +++ b/src/web/public/panels-ui.js @@ -670,7 +670,7 @@ Object.assign(CodemanApp.prototype, { } else if (record.workingDir) { // History rows are keyed by the Claude conversation UUID; resumed // sessions carry theirs separately as claudeSessionId. - void this.resumeHistorySession(s.claudeSessionId || s.sessionId, record.workingDir); + void this.resumeHistorySession(s.claudeSessionId || s.sessionId, record.workingDir, undefined, s.mode); } }, }); diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index b78ce074..b4d18479 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -692,7 +692,7 @@ Object.assign(CodemanApp.prototype, { btn.append(...parts); btn.addEventListener('click', (e) => { e.stopPropagation(); - this.resumeHistorySession(s.sessionId, s.workingDir, s.name); + this.resumeHistorySession(s.sessionId, s.workingDir, s.name, s.mode); }); container.appendChild(btn); } diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index 84451307..a3feb0fc 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -2193,7 +2193,7 @@ Object.assign(CodemanApp.prototype, { if (isLive && this.sessions.has(s.sessionId)) { this.selectSession(s.sessionId); } else { - this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name); + this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name, s.mode); } }) ); @@ -2436,7 +2436,7 @@ Object.assign(CodemanApp.prototype, { } else { // Resume by the Claude conversation UUID when present (resumed sessions // carry theirs separately from their Codeman id). - this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name); + this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name, s.mode); } this.closeSessionManager?.(); closeMenu(); @@ -2904,7 +2904,7 @@ Object.assign(CodemanApp.prototype, { return `w${startNumber}-${dirName}`; }, - async resumeHistorySession(sessionId, workingDir, existingName) { + async resumeHistorySession(sessionId, workingDir, existingName, mode) { // Close the run mode menu if open document.getElementById('runModeMenu')?.classList.remove('active'); // Close folder history modal if open @@ -2925,13 +2925,38 @@ Object.assign(CodemanApp.prototype, { const globalSettings = this.loadAppSettingsFromStorage(); const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings); const effort = this.getEffortSetting(globalSettings); + // `resumeSessionId` is a Claude conversation UUID (server reads it from + // ~/.claude/projects); an external-CLI row has no such thing, so sending + // it there gets silently ignored while the OMITTED `mode` field defaults + // the create to plain claude — reproducing whatever conversation THAT + // uuid happens to collide with instead of the row's own backend. Row mode + // wins here. Codeman has no cross-restart PTY-reattach outside server + // boot, so "resume" for a non-claude row means relaunching the CLI's own + // continue-most-recent flag (opencode/pi/grok/omp --continue, deepseek + // resumeSession) in the same directory — real conversation continuity, + // just not the literal old process. + const effectiveMode = mode || 'claude'; + const modeConfigKey = { + opencode: 'openCodeConfig', + pi: 'piConfig', + grok: 'grokConfig', + omp: 'ompConfig', + }[effectiveMode]; + const modeConfig = + modeConfigKey + ? { [modeConfigKey]: { continueSession: true } } + : effectiveMode === 'deepseek' + ? { deepSeekConfig: { resumeSession: true } } + : {}; const createRes = await fetch('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ workingDir, name, - resumeSessionId: sessionId, + mode: effectiveMode, + ...(effectiveMode === 'claude' ? { resumeSessionId: sessionId } : {}), + ...modeConfig, ...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}), ...(effort ? { effort } : {}), }), diff --git a/src/web/schemas.ts b/src/web/schemas.ts index e7401783..c7c015ef 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -361,6 +361,7 @@ const OmpConfigSchema = z .max(100) .regex(/^[a-zA-Z0-9._-]+$/) .optional(), + continueSession: z.boolean().optional(), }) .optional(); diff --git a/test/command-palette-ui.test.ts b/test/command-palette-ui.test.ts index de8df0a2..263d9b6a 100644 --- a/test/command-palette-ui.test.ts +++ b/test/command-palette-ui.test.ts @@ -401,7 +401,7 @@ describe('Session Manager unified list', () => { const [historyRecord, , historyOptions] = app._buildHistoryItem.mock.calls[1]; expect(historyRecord).toMatchObject({ sessionId: 'conv-uuid-1', sizeBytes: 2048, firstPrompt: 'old prompt' }); historyOptions.onActivate(); - expect(app.resumeHistorySession).toHaveBeenCalledWith('conv-uuid-1', '/repo/old'); + expect(app.resumeHistorySession).toHaveBeenCalledWith('conv-uuid-1', '/repo/old', undefined, undefined); }); it('surfaces an error message instead of an empty list when the endpoint fails', async () => { diff --git a/test/omp-mode.test.ts b/test/omp-mode.test.ts index c7d3ff39..941b8cd0 100644 --- a/test/omp-mode.test.ts +++ b/test/omp-mode.test.ts @@ -85,6 +85,26 @@ describe('OMP spawn command', () => { ).toBe('omp'); }); + it('continues the most recent session when no explicit resume id is given', () => { + expect( + buildSpawnCommand({ + mode: 'omp', + sessionId: 'abc12345', + ompConfig: { continueSession: true }, + }) + ).toBe('omp --continue'); + }); + + it('prefers an explicit --resume id over --continue', () => { + expect( + buildSpawnCommand({ + mode: 'omp', + sessionId: 'abc12345', + ompConfig: { resumeSessionId: 'session-99', continueSession: true }, + }) + ).toBe('omp --resume session-99'); + }); + it('drops unsafe model strings from the spawn command', () => { expect( buildSpawnCommand({