mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
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.
This commit is contained in:
+8
-3
@@ -914,9 +914,14 @@ function buildOmpCommand(config?: OmpConfig): string {
|
|||||||
if (safeModel) parts.push('--model', safeModel);
|
if (safeModel) parts.push('--model', safeModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config?.resumeSessionId) {
|
// --resume and --continue conflict; a valid explicit session id wins,
|
||||||
const safeId = /^[a-zA-Z0-9._-]+$/.test(config.resumeSessionId) ? config.resumeSessionId : undefined;
|
// mirroring the sibling builders (grok/pi/opencode).
|
||||||
if (safeId) parts.push('--resume', safeId);
|
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(' ');
|
return parts.join(' ');
|
||||||
|
|||||||
@@ -350,6 +350,8 @@ export interface OmpConfig {
|
|||||||
model?: string;
|
model?: string;
|
||||||
/** Resume a previous conversation (passed via --resume). */
|
/** Resume a previous conversation (passed via --resume). */
|
||||||
resumeSessionId?: string;
|
resumeSessionId?: string;
|
||||||
|
/** Continue the most recent session in this directory (passed via --continue). */
|
||||||
|
continueSession?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
async resumeMobileOverviewSession(sessionId) {
|
async resumeMobileOverviewSession(sessionId) {
|
||||||
const row = (this._mobileOverviewPastRows || []).find((r) => r.id === sessionId);
|
const row = (this._mobileOverviewPastRows || []).find((r) => r.id === sessionId);
|
||||||
if (!row || !row.workingDir) return;
|
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
} else if (record.workingDir) {
|
} else if (record.workingDir) {
|
||||||
// History rows are keyed by the Claude conversation UUID; resumed
|
// History rows are keyed by the Claude conversation UUID; resumed
|
||||||
// sessions carry theirs separately as claudeSessionId.
|
// 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -692,7 +692,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
btn.append(...parts);
|
btn.append(...parts);
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.resumeHistorySession(s.sessionId, s.workingDir, s.name);
|
this.resumeHistorySession(s.sessionId, s.workingDir, s.name, s.mode);
|
||||||
});
|
});
|
||||||
container.appendChild(btn);
|
container.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2193,7 +2193,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (isLive && this.sessions.has(s.sessionId)) {
|
if (isLive && this.sessions.has(s.sessionId)) {
|
||||||
this.selectSession(s.sessionId);
|
this.selectSession(s.sessionId);
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
// Resume by the Claude conversation UUID when present (resumed sessions
|
// Resume by the Claude conversation UUID when present (resumed sessions
|
||||||
// carry theirs separately from their Codeman id).
|
// 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?.();
|
this.closeSessionManager?.();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
@@ -2904,7 +2904,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
return `w${startNumber}-${dirName}`;
|
return `w${startNumber}-${dirName}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
async resumeHistorySession(sessionId, workingDir, existingName) {
|
async resumeHistorySession(sessionId, workingDir, existingName, mode) {
|
||||||
// Close the run mode menu if open
|
// Close the run mode menu if open
|
||||||
document.getElementById('runModeMenu')?.classList.remove('active');
|
document.getElementById('runModeMenu')?.classList.remove('active');
|
||||||
// Close folder history modal if open
|
// Close folder history modal if open
|
||||||
@@ -2925,13 +2925,38 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const globalSettings = this.loadAppSettingsFromStorage();
|
const globalSettings = this.loadAppSettingsFromStorage();
|
||||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
|
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
|
||||||
const effort = this.getEffortSetting(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', {
|
const createRes = await fetch('/api/sessions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
workingDir,
|
workingDir,
|
||||||
name,
|
name,
|
||||||
resumeSessionId: sessionId,
|
mode: effectiveMode,
|
||||||
|
...(effectiveMode === 'claude' ? { resumeSessionId: sessionId } : {}),
|
||||||
|
...modeConfig,
|
||||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||||
...(effort ? { effort } : {}),
|
...(effort ? { effort } : {}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ const OmpConfigSchema = z
|
|||||||
.max(100)
|
.max(100)
|
||||||
.regex(/^[a-zA-Z0-9._-]+$/)
|
.regex(/^[a-zA-Z0-9._-]+$/)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
continueSession: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ describe('Session Manager unified list', () => {
|
|||||||
const [historyRecord, , historyOptions] = app._buildHistoryItem.mock.calls[1];
|
const [historyRecord, , historyOptions] = app._buildHistoryItem.mock.calls[1];
|
||||||
expect(historyRecord).toMatchObject({ sessionId: 'conv-uuid-1', sizeBytes: 2048, firstPrompt: 'old prompt' });
|
expect(historyRecord).toMatchObject({ sessionId: 'conv-uuid-1', sizeBytes: 2048, firstPrompt: 'old prompt' });
|
||||||
historyOptions.onActivate();
|
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 () => {
|
it('surfaces an error message instead of an empty list when the endpoint fails', async () => {
|
||||||
|
|||||||
@@ -85,6 +85,26 @@ describe('OMP spawn command', () => {
|
|||||||
).toBe('omp');
|
).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', () => {
|
it('drops unsafe model strings from the spawn command', () => {
|
||||||
expect(
|
expect(
|
||||||
buildSpawnCommand({
|
buildSpawnCommand({
|
||||||
|
|||||||
Reference in New Issue
Block a user