mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix(codex): limit launch defaults to local sessions
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
'aicodeman': minor
|
'aicodeman': minor
|
||||||
---
|
---
|
||||||
|
|
||||||
Add synced Codex default model and reasoning effort controls to App Settings. Apply defaults to new local and Docker sessions while preserving explicit launch settings, custom endpoints, remote commands, and Codex configuration when defaults are empty.
|
Add synced Codex default model and reasoning effort controls to App Settings. Apply defaults to new local sessions (including WSL) while preserving explicit launch settings, custom endpoints, Docker and remote commands, and Codex configuration when defaults are empty.
|
||||||
|
|||||||
@@ -128,9 +128,9 @@ Integration detail: [`docs/opencode-integration.md`](https://github.com/Ark0N/Co
|
|||||||
App Settings has synced **Default Codex model** and **Default Codex reasoning effort**
|
App Settings has synced **Default Codex model** and **Default Codex reasoning effort**
|
||||||
controls. Enter a model ID supported by your Codex provider; available reasoning levels
|
controls. Enter a model ID supported by your Codex provider; available reasoning levels
|
||||||
depend on the model and CLI version. Empty defaults use Codex's own configuration.
|
depend on the model and CLI version. Empty defaults use Codex's own configuration.
|
||||||
The defaults apply when creating local or Docker sessions, including HTTP API launches.
|
The defaults apply when creating local sessions (including WSL), including HTTP API launches.
|
||||||
Explicit `codexConfig.model` / `codexConfig.reasoningEffort` values take precedence.
|
Explicit `codexConfig.model` / `codexConfig.reasoningEffort` values take precedence.
|
||||||
Custom model endpoints and remote host command overrides keep their own settings.
|
Custom model endpoints, Docker containers and remote host command overrides keep their own settings.
|
||||||
Changing a default affects new sessions and does not edit Codex configuration files.
|
Changing a default affects new sessions and does not edit Codex configuration files.
|
||||||
|
|
||||||
Two behaviours that are deliberate and worth knowing:
|
Two behaviours that are deliberate and worth knowing:
|
||||||
|
|||||||
@@ -48,10 +48,10 @@
|
|||||||
'Default Codex model': 'Codex 默认模型',
|
'Default Codex model': 'Codex 默认模型',
|
||||||
'Default Codex reasoning effort': 'Codex 默认思考强度',
|
'Default Codex reasoning effort': 'Codex 默认思考强度',
|
||||||
'Use Codex configuration': '使用 Codex 配置',
|
'Use Codex configuration': '使用 Codex 配置',
|
||||||
'Model ID for new Codex sessions. Leave empty to use Codex configuration.':
|
'Model ID for new local Codex sessions, including WSL. Leave empty to use Codex configuration.':
|
||||||
'新 Codex 会话使用的模型 ID。留空时使用 Codex 配置。',
|
'新本地 Codex 会话(包括 WSL)使用的模型 ID。留空时使用 Codex 配置。',
|
||||||
'Applies to new sessions; supported levels depend on the model and Codex version. Custom endpoints keep their own settings.':
|
'Applies to new local sessions; supported levels depend on the model and Codex version. Custom endpoints, Docker and remote sessions keep their own settings.':
|
||||||
'应用于新会话;可用强度取决于模型和 Codex 版本。自定义端点保留自己的设置。',
|
'应用于新本地会话;可用强度取决于模型和 Codex 版本。自定义端点、Docker 和远程会话保留自己的设置。',
|
||||||
'Skip to terminal': '跳转到终端',
|
'Skip to terminal': '跳转到终端',
|
||||||
'Go to main page': '返回主页',
|
'Go to main page': '返回主页',
|
||||||
'Session tabs': '会话标签页',
|
'Session tabs': '会话标签页',
|
||||||
|
|||||||
@@ -2530,14 +2530,14 @@
|
|||||||
<div class="set-row" data-search="codex default model">
|
<div class="set-row" data-search="codex default model">
|
||||||
<div class="set-row-text">
|
<div class="set-row-text">
|
||||||
<span class="set-row-label">Default Codex model</span>
|
<span class="set-row-label">Default Codex model</span>
|
||||||
<span class="set-row-desc">Model ID for new Codex sessions. Leave empty to use Codex configuration.</span>
|
<span class="set-row-desc">Model ID for new local Codex sessions, including WSL. Leave empty to use Codex configuration.</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="appSettingsCodexModel" class="set-input" type="text" maxlength="100" aria-label="Default Codex model" placeholder="Use Codex configuration" autocomplete="off" spellcheck="false">
|
<input id="appSettingsCodexModel" class="set-input" type="text" maxlength="100" aria-label="Default Codex model" placeholder="Use Codex configuration" autocomplete="off" spellcheck="false">
|
||||||
</div>
|
</div>
|
||||||
<div class="set-row" data-search="codex default reasoning effort thinking">
|
<div class="set-row" data-search="codex default reasoning effort thinking">
|
||||||
<div class="set-row-text">
|
<div class="set-row-text">
|
||||||
<span class="set-row-label">Default Codex reasoning effort</span>
|
<span class="set-row-label">Default Codex reasoning effort</span>
|
||||||
<span class="set-row-desc">Applies to new sessions; supported levels depend on the model and Codex version. Custom endpoints keep their own settings.</span>
|
<span class="set-row-desc">Applies to new local sessions; supported levels depend on the model and Codex version. Custom endpoints, Docker and remote sessions keep their own settings.</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="appSettingsCodexReasoningEffort" class="set-select" aria-label="Default Codex reasoning effort">
|
<select id="appSettingsCodexReasoningEffort" class="set-select" aria-label="Default Codex reasoning effort">
|
||||||
<option value="">Use Codex configuration</option>
|
<option value="">Use Codex configuration</option>
|
||||||
|
|||||||
@@ -1104,7 +1104,8 @@ export function registerSessionRoutes(
|
|||||||
const globalNice = await ctx.getGlobalNiceConfig();
|
const globalNice = await ctx.getGlobalNiceConfig();
|
||||||
const modelConfig = await ctx.getModelConfig();
|
const modelConfig = await ctx.getModelConfig();
|
||||||
const mode = body.mode || 'claude';
|
const mode = body.mode || 'claude';
|
||||||
const launchCodexConfig = mode === 'codex' ? await resolveCodexLaunchDefaults(body.codexConfig) : body.codexConfig;
|
const launchCodexConfig =
|
||||||
|
mode === 'codex' && !remote ? await resolveCodexLaunchDefaults(body.codexConfig) : body.codexConfig;
|
||||||
// Where a model override comes from is a capability, and the three answers are
|
// Where a model override comes from is a capability, and the three answers are
|
||||||
// genuinely different mechanisms:
|
// genuinely different mechanisms:
|
||||||
// 'flag' — the CLI takes --model, so read the value the caller sent
|
// 'flag' — the CLI takes --model, so read the value the caller sent
|
||||||
@@ -3835,7 +3836,9 @@ export function registerSessionRoutes(
|
|||||||
const niceConfig = await ctx.getGlobalNiceConfig();
|
const niceConfig = await ctx.getGlobalNiceConfig();
|
||||||
const qsModelConfig = await ctx.getModelConfig();
|
const qsModelConfig = await ctx.getModelConfig();
|
||||||
const qsLaunchCodexConfig =
|
const qsLaunchCodexConfig =
|
||||||
mode === 'codex' && !remote ? await resolveCodexLaunchDefaults(codexConfig, !!customModel) : codexConfig;
|
mode === 'codex' && !remote && !docker
|
||||||
|
? await resolveCodexLaunchDefaults(codexConfig, !!customModel)
|
||||||
|
: codexConfig;
|
||||||
// See the create path for why this is a capability rather than a mode ladder.
|
// See the create path for why this is a capability rather than a mode ladder.
|
||||||
const qsModelSource = getCli(mode)?.capabilities.model;
|
const qsModelSource = getCli(mode)?.capabilities.model;
|
||||||
const qsModel =
|
const qsModel =
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { mkdir, writeFile } from 'node:fs/promises';
|
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { createRouteTestHarness, type RouteTestHarness } from './_route-test-utils.js';
|
import { createRouteTestHarness, type RouteTestHarness } from './_route-test-utils.js';
|
||||||
@@ -10,6 +10,7 @@ import { resolveCodexLaunchDefaults } from '../../src/web/codex-launch-defaults.
|
|||||||
import { buildCodexCommand } from '../../src/tmux-manager.js';
|
import { buildCodexCommand } from '../../src/tmux-manager.js';
|
||||||
import { Session } from '../../src/session.js';
|
import { Session } from '../../src/session.js';
|
||||||
import { safeRmHomeTree } from '../mocks/index.js';
|
import { safeRmHomeTree } from '../mocks/index.js';
|
||||||
|
import { getDataDir } from '../../src/config/instance.js';
|
||||||
|
|
||||||
vi.mock('../../src/utils/cli-launcher.js', async (importOriginal) => {
|
vi.mock('../../src/utils/cli-launcher.js', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('../../src/utils/cli-launcher.js')>();
|
const actual = await importOriginal<typeof import('../../src/utils/cli-launcher.js')>();
|
||||||
@@ -21,6 +22,9 @@ describe('Codex launch defaults', () => {
|
|||||||
const workingDir = join(homedir(), 'codex-default-test');
|
const workingDir = join(homedir(), 'codex-default-test');
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
for (const name of ['docker-hosts.json', 'docker-cases.json', 'remote-hosts.json', 'remote-cases.json']) {
|
||||||
|
await rm(join(getDataDir(), name), { force: true });
|
||||||
|
}
|
||||||
await mkdir(workingDir, { recursive: true });
|
await mkdir(workingDir, { recursive: true });
|
||||||
await mkdir(dirname(SETTINGS_PATH), { recursive: true });
|
await mkdir(dirname(SETTINGS_PATH), { recursive: true });
|
||||||
await writeFile(SETTINGS_PATH, JSON.stringify({ codexModel: 'gpt-6.1', codexReasoningEffort: 'high' }));
|
await writeFile(SETTINGS_PATH, JSON.stringify({ codexModel: 'gpt-6.1', codexReasoningEffort: 'high' }));
|
||||||
@@ -42,6 +46,7 @@ describe('Codex launch defaults', () => {
|
|||||||
: { caseName: 'codex-default-test', mode: 'codex', ...overrides };
|
: { caseName: 'codex-default-test', mode: 'codex', ...overrides };
|
||||||
const res = await harness.app.inject({ method: 'POST', url, payload });
|
const res = await harness.app.inject({ method: 'POST', url, payload });
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.json().success, res.body).not.toBe(false);
|
||||||
const session = [...harness.ctx.sessions.values()].at(-1) as Session | undefined;
|
const session = [...harness.ctx.sessions.values()].at(-1) as Session | undefined;
|
||||||
expect(session).toBeDefined();
|
expect(session).toBeDefined();
|
||||||
return session!.codexConfig;
|
return session!.codexConfig;
|
||||||
@@ -79,6 +84,60 @@ describe('Codex launch defaults', () => {
|
|||||||
expect(await resolveCodexLaunchDefaults(config, true)).toBe(config);
|
expect(await resolveCodexLaunchDefaults(config, true)).toBe(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not record unused defaults for a Docker quick-start', async () => {
|
||||||
|
await writeFile(
|
||||||
|
join(getDataDir(), 'docker-hosts.json'),
|
||||||
|
JSON.stringify([{ id: 'docker1', label: 'Docker', image: 'codeman/agent:base' }])
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(getDataDir(), 'docker-cases.json'),
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
name: 'codex-default-test',
|
||||||
|
type: 'docker',
|
||||||
|
hostId: 'docker1',
|
||||||
|
hostWorkspacePath: workingDir,
|
||||||
|
container: 'codeman-default-test',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(await createdConfig('/api/quick-start')).toBeUndefined();
|
||||||
|
const session = [...harness.ctx.sessions.values()].at(-1) as Session;
|
||||||
|
expect(session.docker?.containerName).toBe('codeman-default-test');
|
||||||
|
expect(session.toState().model).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not record unused defaults for a remote attach', async () => {
|
||||||
|
await writeFile(
|
||||||
|
join(getDataDir(), 'remote-hosts.json'),
|
||||||
|
JSON.stringify([{ id: 'remote1', label: 'Remote', host: '10.0.0.5', username: 'dev' }])
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await createdConfig('/api/sessions', {
|
||||||
|
workingDir: undefined,
|
||||||
|
attachRemoteSession: { hostId: 'remote1', remoteSessionName: 'codeman-existing' },
|
||||||
|
})
|
||||||
|
).toBeUndefined();
|
||||||
|
const session = [...harness.ctx.sessions.values()].at(-1) as Session;
|
||||||
|
expect(session.toState().remote?.hostId).toBe('remote1');
|
||||||
|
expect(session.toState().model).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not record unused defaults for a remote quick-start', async () => {
|
||||||
|
await writeFile(
|
||||||
|
join(getDataDir(), 'remote-hosts.json'),
|
||||||
|
JSON.stringify([{ id: 'remote1', label: 'Remote', host: '10.0.0.5', username: 'dev' }])
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(getDataDir(), 'remote-cases.json'),
|
||||||
|
JSON.stringify([{ name: 'codex-default-test', type: 'remote', hostId: 'remote1', remotePath: '/home/dev/work' }])
|
||||||
|
);
|
||||||
|
expect(await createdConfig('/api/quick-start')).toBeUndefined();
|
||||||
|
const session = [...harness.ctx.sessions.values()].at(-1) as Session;
|
||||||
|
expect(session.toState().remote?.hostId).toBe('remote1');
|
||||||
|
expect(session.toState().model).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts, persists, clears and validates synced settings via HTTP', async () => {
|
it('accepts, persists, clears and validates synced settings via HTTP', async () => {
|
||||||
const system = await createRouteTestHarness(registerSystemRoutes);
|
const system = await createRouteTestHarness(registerSystemRoutes);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user