feat: refresh dashboard settings UX and Codex feature toggles (#153)

* feat: refresh dashboard settings UX and Codex feature toggles

Improve settings/tasks mobile layout, model effort validation by agent type, and preset models for GPT 5.5 and Opus 4.7. Store Codex fast mode and goals in config.toml with Claude fastMode session sync and updated docs for Codex 0.133.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: apply pre-commit formatting after dashboard settings commit

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Eyejoker
2026-05-23 12:18:42 +09:00
committed by GitHub
parent 58e5197dc6
commit fd3145e2a7
37 changed files with 3785 additions and 1910 deletions

View File

@@ -1,4 +1,5 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -577,6 +578,24 @@ describe('prepareGroupEnvironment Codex goals handling', () => {
expect(prepared.env.CODEX_GOALS).toBe('true');
});
it('enables goals from host ~/.codex/config.toml [features]', () => {
mockReadEnvFile.mockReturnValue({});
const homedirSpy = vi
.spyOn(os, 'homedir')
.mockReturnValue(process.env.EJ_TEST_HOME!);
fs.writeFileSync(
path.join(process.env.EJ_TEST_HOME!, '.codex', 'config.toml'),
'[features]\ngoals = true\n',
);
try {
const prepared = prepareGroupEnvironment(group, false, 'dc:test');
expect(prepared.env.CODEX_GOALS).toBe('true');
} finally {
homedirSpy.mockRestore();
}
});
});
describe('prepareReadonlySessionEnvironment codex compatibility', () => {

View File

@@ -12,6 +12,8 @@ import {
import { logger } from './logger.js';
import { readEnvFile } from './env.js';
import { getActiveCodexAuthPath } from './codex-token-rotation.js';
import { readCodexFeatureFromFile } from './codex-config-features.js';
import { ensureClaudeSessionSettings } from './claude-session-settings.js';
import {
getConfiguredClaudeTokens,
getCurrentToken,
@@ -130,27 +132,7 @@ function readOptionalPromptFile(
return prompt || undefined;
}
function ensureClaudeSessionSettings(groupSessionsDir: string): void {
const settingsFile = path.join(groupSessionsDir, 'settings.json');
if (fs.existsSync(settingsFile)) return;
fs.writeFileSync(
settingsFile,
JSON.stringify(
{
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
},
},
null,
2,
) + '\n',
);
}
export function ensureClaudeGlobalSettingsFile(sessionDir: string): void {
function ensureClaudeGlobalSettingsFile(sessionDir: string): void {
const settingsFile = path.join(sessionDir, '.claude.json');
if (fs.existsSync(settingsFile)) return;
@@ -387,15 +369,6 @@ function prepareCodexSessionEnvironment(args: {
process.env.CODEX_EFFORT;
if (codexEffort) args.env.CODEX_EFFORT = codexEffort;
const codexGoals =
args.group.agentConfig?.codexGoals ??
(args.envVars.CODEX_GOALS === 'true' || process.env.CODEX_GOALS === 'true');
if (codexGoals) {
args.env.CODEX_GOALS = 'true';
} else {
delete args.env.CODEX_GOALS;
}
const sessionCodexDir = path.join(args.sessionRootDir, '.codex');
syncHostCodexSessionFiles(sessionCodexDir);
@@ -414,6 +387,18 @@ function prepareCodexSessionEnvironment(args: {
}
}
const goalsFromConfig = readCodexFeatureFromFile(sessionConfigPath, 'goals');
const codexGoals =
args.group.agentConfig?.codexGoals ??
(goalsFromConfig ||
args.envVars.CODEX_GOALS === 'true' ||
process.env.CODEX_GOALS === 'true');
if (codexGoals) {
args.env.CODEX_GOALS = 'true';
} else {
delete args.env.CODEX_GOALS;
}
const sessionAgentsPath = path.join(sessionCodexDir, 'AGENTS.md');
const sessionAgents = (
args.useFailoverPromptPack

View File

@@ -53,6 +53,7 @@ vi.mock('../service-routing.js', () => ({
type Handler = (...args: any[]) => any;
const clientRef = vi.hoisted(() => ({ current: null as any }));
const loginShouldRejectRef = vi.hoisted(() => ({ value: false }));
vi.mock('discord.js', () => {
const Events = {
@@ -89,6 +90,9 @@ vi.mock('discord.js', () => {
}
async login(_token: string) {
if (loginShouldRejectRef.value) {
throw new Error('An invalid token was provided.');
}
this._ready = true;
// Fire the ready event
const readyHandlers = this.eventHandlers.get('ready') || [];
@@ -233,6 +237,7 @@ describe('DiscordChannel', () => {
beforeEach(() => {
vi.clearAllMocks();
hasReviewerLeaseMock.mockReturnValue(false);
loginShouldRejectRef.value = false;
});
afterEach(() => {
@@ -263,6 +268,17 @@ describe('DiscordChannel', () => {
expect(channel.isConnected()).toBe(true);
});
it('rejects connect() when login fails', async () => {
loginShouldRejectRef.value = true;
const opts = createTestOpts();
const channel = new DiscordChannel('bad-token', opts);
await expect(channel.connect()).rejects.toThrow(
'An invalid token was provided.',
);
expect(channel.isConnected()).toBe(false);
});
it('registers message handlers on connect', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);

View File

@@ -239,7 +239,7 @@ export class DiscordChannel implements Channel {
logger.error({ err: err.message }, 'Discord client error');
});
return new Promise<void>((resolve) => {
return new Promise<void>((resolve, reject) => {
this.client!.once(Events.ClientReady, (readyClient) => {
logger.info(
{ username: readyClient.user.tag, id: readyClient.user.id },
@@ -252,7 +252,7 @@ export class DiscordChannel implements Channel {
resolve();
});
this.client!.login(this.botToken);
void this.client!.login(this.botToken).catch(reject);
});
}

View File

@@ -0,0 +1,39 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { ensureClaudeSessionSettings } from './claude-session-settings.js';
describe('claude-session-settings', () => {
let tempDir: string;
let hostSettingsPath: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-claude-settings-'));
hostSettingsPath = path.join(tempDir, 'host-settings.json');
process.env.EJCLAW_CLAUDE_SETTINGS_PATH = hostSettingsPath;
});
afterEach(() => {
delete process.env.EJCLAW_CLAUDE_SETTINGS_PATH;
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('syncs host fastMode into the Claude session settings file', () => {
fs.writeFileSync(
hostSettingsPath,
`${JSON.stringify({ fastMode: true }, null, 2)}\n`,
);
const sessionDir = path.join(tempDir, 'session');
ensureClaudeSessionSettings(sessionDir);
const session = JSON.parse(
fs.readFileSync(path.join(sessionDir, 'settings.json'), 'utf-8'),
) as { fastMode?: boolean; env?: Record<string, string> };
expect(session.fastMode).toBe(true);
expect(session.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBe('1');
});
});

View File

@@ -0,0 +1,68 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const DEFAULT_CLAUDE_SESSION_ENV = {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
} as const;
function settingsHomeDir(): string {
return process.env.EJCLAW_SETTINGS_HOME || os.homedir();
}
export function claudeHostSettingsPath(): string {
const override = process.env.EJCLAW_CLAUDE_SETTINGS_PATH?.trim();
if (override) return override;
return path.join(settingsHomeDir(), '.claude', 'settings.json');
}
export function readHostClaudeFastMode(): boolean {
const file = claudeHostSettingsPath();
if (!fs.existsSync(file)) return false;
try {
const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<
string,
unknown
>;
return data.fastMode === true;
} catch {
return false;
}
}
export function ensureClaudeSessionSettings(groupSessionsDir: string): void {
const settingsFile = path.join(groupSessionsDir, 'settings.json');
let data: Record<string, unknown> = {
env: { ...DEFAULT_CLAUDE_SESSION_ENV },
};
if (fs.existsSync(settingsFile)) {
try {
const existing = JSON.parse(
fs.readFileSync(settingsFile, 'utf-8'),
) as Record<string, unknown>;
data = {
...existing,
env: {
...DEFAULT_CLAUDE_SESSION_ENV,
...(typeof existing.env === 'object' && existing.env !== null
? (existing.env as Record<string, unknown>)
: {}),
},
};
} catch {
data = { env: { ...DEFAULT_CLAUDE_SESSION_ENV } };
}
}
if (readHostClaudeFastMode()) {
data.fastMode = true;
} else {
delete data.fastMode;
}
fs.mkdirSync(groupSessionsDir, { recursive: true });
fs.writeFileSync(settingsFile, `${JSON.stringify(data, null, 2)}\n`);
}

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import {
readCodexFeatureFromContent,
writeCodexFeatureInContent,
} from './codex-config-features.js';
describe('codex-config-features', () => {
it('reads feature flags from the [features] section', () => {
const toml = `
model = "gpt-5.5"
[features]
fast_mode = true
goals = false
`;
expect(readCodexFeatureFromContent(toml, 'fast_mode')).toBe(true);
expect(readCodexFeatureFromContent(toml, 'goals')).toBe(false);
});
it('writes missing feature flags into [features]', () => {
const updated = writeCodexFeatureInContent(
'model = "gpt-5.5"\n',
'goals',
true,
);
expect(updated).toContain('[features]');
expect(updated).toContain('goals = true');
});
});

View File

@@ -0,0 +1,84 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
export type CodexConfigFeature = 'fast_mode' | 'goals';
export function codexConfigPath(): string {
const override = process.env.EJCLAW_CODEX_CONFIG_PATH?.trim();
if (override) return override;
const home = process.env.EJCLAW_SETTINGS_HOME || os.homedir();
return path.join(home, '.codex', 'config.toml');
}
export function readCodexFeatureFromContent(
content: string,
feature: CodexConfigFeature,
): boolean {
const lines = content.split('\n');
let inFeatures = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '[features]') {
inFeatures = true;
continue;
}
if (inFeatures && trimmed.startsWith('[') && trimmed.endsWith(']')) {
break;
}
if (!inFeatures) continue;
const match = trimmed.match(
new RegExp(`^${feature}\\s*=\\s*(true|false)$`),
);
if (match) return match[1] === 'true';
}
return false;
}
export function readCodexFeatureFromFile(
filePath: string,
feature: CodexConfigFeature,
): boolean {
if (!fs.existsSync(filePath)) return false;
return readCodexFeatureFromContent(
fs.readFileSync(filePath, 'utf-8'),
feature,
);
}
export function writeCodexFeatureInContent(
content: string,
feature: CodexConfigFeature,
value: boolean,
): string {
const line = `${feature} = ${value}`;
const re = new RegExp(`^\\s*${feature}\\s*=\\s*(true|false)\\s*$`, 'm');
if (re.test(content)) {
return content.replace(re, line);
}
if (/^\[features\]/m.test(content)) {
return content.replace(/^\[features\]\s*$/m, `[features]\n${line}`);
}
const trimmed = content.replace(/\s*$/, '');
if (!trimmed) {
return `[features]\n${line}\n`;
}
return `${trimmed}\n\n[features]\n${line}\n`;
}
export function writeCodexFeatureToFile(
filePath: string,
feature: CodexConfigFeature,
value: boolean,
): void {
const content = fs.existsSync(filePath)
? fs.readFileSync(filePath, 'utf-8')
: '';
const updated = writeCodexFeatureInContent(content, feature, value);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const tempPath = `${filePath}.tmp`;
fs.writeFileSync(tempPath, updated, { mode: 0o600 });
fs.renameSync(tempPath, filePath);
}

View File

@@ -346,12 +346,25 @@ async function main(): Promise<void> {
);
continue;
}
channels.push(channel);
await channel.connect();
try {
await channel.connect();
channels.push(channel);
} catch (err) {
logger.error(
{ channel: channelName, err },
'Channel connect failed — skipping',
);
}
}
if (channels.length === 0) {
logger.fatal('No channels connected');
process.exit(1);
if (WEB_DASHBOARD.enabled) {
logger.warn(
'No channels connected; continuing in web-dashboard-only mode',
);
} else {
logger.fatal('No channels connected');
process.exit(1);
}
}
// Start subsystems (independently of connection handler)

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { agentTypeForRole, isEffortSupported } from './settings-effort.js';
describe('settings-effort', () => {
it('maps roles to agent types with defaults', () => {
expect(agentTypeForRole('owner', {})).toBe('codex');
expect(agentTypeForRole('reviewer', {})).toBe('claude-code');
expect(agentTypeForRole('arbiter', {})).toBeNull();
expect(agentTypeForRole('owner', { OWNER_AGENT_TYPE: 'claude-code' })).toBe(
'claude-code',
);
});
it('rejects xhigh for Claude agents', () => {
expect(isEffortSupported('claude-code', 'xhigh')).toBe(false);
expect(isEffortSupported('codex', 'xhigh')).toBe(true);
expect(isEffortSupported('claude-code', '')).toBe(true);
});
});

64
src/settings-effort.ts Normal file
View File

@@ -0,0 +1,64 @@
export type SettingsAgentType = 'claude-code' | 'codex';
export const CODEX_EFFORT_VALUES = [
'',
'low',
'medium',
'high',
'xhigh',
'max',
] as const;
export const CLAUDE_EFFORT_VALUES = [
'',
'low',
'medium',
'high',
'max',
] as const;
export type EffortValue = (typeof CODEX_EFFORT_VALUES)[number];
export function effortValuesForAgent(
agentType: SettingsAgentType,
): readonly EffortValue[] {
return agentType === 'claude-code'
? CLAUDE_EFFORT_VALUES
: CODEX_EFFORT_VALUES;
}
export function isEffortSupported(
agentType: SettingsAgentType | null | undefined,
effort: string,
): boolean {
if (!effort) return true;
if (!agentType) return true;
return effortValuesForAgent(agentType).includes(effort as EffortValue);
}
export function readSettingsAgentType(
value: string | undefined,
): SettingsAgentType | null {
if (value === 'claude-code' || value === 'codex') return value;
return null;
}
export function agentTypeForRole(
role: 'owner' | 'reviewer' | 'arbiter',
env:
| Record<string, string | undefined>
| ((key: string) => string | undefined),
): SettingsAgentType | null {
const read = typeof env === 'function' ? env : (key: string) => env[key];
const key =
role === 'owner'
? 'OWNER_AGENT_TYPE'
: role === 'reviewer'
? 'REVIEWER_AGENT_TYPE'
: 'ARBITER_AGENT_TYPE';
const raw = read(key);
if (raw !== undefined) return readSettingsAgentType(raw);
if (role === 'owner') return 'codex';
if (role === 'reviewer') return 'claude-code';
return null;
}

View File

@@ -29,6 +29,7 @@ describe('settings-store Codex features', () => {
delete process.env.CODEX_GOALS;
process.env.HOME = tempDir;
process.env.EJCLAW_SETTINGS_HOME = tempDir;
fs.mkdirSync(path.join(tempDir, '.codex'), { recursive: true });
process.chdir(tempDir);
});
@@ -59,17 +60,28 @@ describe('settings-store Codex features', () => {
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.`;
}
it('stores the Codex goals opt-in in the EJClaw .env file', () => {
it('stores Codex goals in ~/.codex/config.toml [features]', () => {
expect(getCodexFeatures()).toEqual({ goals: false });
expect(updateCodexFeatures({ goals: true })).toEqual({ goals: true });
expect(fs.readFileSync(path.join(tempDir, '.env'), 'utf-8')).toContain(
'CODEX_GOALS=true',
);
expect(
fs.readFileSync(path.join(tempDir, '.codex', 'config.toml'), 'utf-8'),
).toContain('goals = true');
expect(updateCodexFeatures({ goals: false })).toEqual({ goals: false });
expect(fs.readFileSync(path.join(tempDir, '.env'), 'utf-8')).toContain(
'CODEX_GOALS=false',
expect(
fs.readFileSync(path.join(tempDir, '.codex', 'config.toml'), 'utf-8'),
).toContain('goals = false');
});
it('still honors legacy CODEX_GOALS=true until migrated', () => {
fs.writeFileSync(path.join(tempDir, '.env'), 'CODEX_GOALS=true\n');
expect(getCodexFeatures()).toEqual({ goals: true });
updateCodexFeatures({ goals: false });
expect(getCodexFeatures()).toEqual({ goals: false });
expect(fs.readFileSync(path.join(tempDir, '.env'), 'utf-8')).not.toContain(
'CODEX_GOALS',
);
});

View File

@@ -26,6 +26,12 @@ import {
type CodexLiveStatus,
type CodexLiveStatusSummary,
} from './codex-live-status.js';
import {
readCodexFeatureFromFile,
writeCodexFeatureToFile,
} from './codex-config-features.js';
import { readHostClaudeFastMode } from './claude-session-settings.js';
import { agentTypeForRole, isEffortSupported } from './settings-effort.js';
export type {
CodexAdditionalRateLimitSummary,
@@ -62,10 +68,17 @@ export interface ModelRoleConfig {
effort: string;
}
export interface ModelAgentTypes {
owner: 'claude-code' | 'codex';
reviewer: 'claude-code' | 'codex';
arbiter: 'claude-code' | 'codex' | null;
}
export interface ModelConfigSnapshot {
owner: ModelRoleConfig;
reviewer: ModelRoleConfig;
arbiter: ModelRoleConfig;
agentTypes: ModelAgentTypes;
}
export interface FastModeSnapshot {
@@ -269,7 +282,19 @@ function readEnvOrProcess(key: string): string | undefined {
}
export function getModelConfig(): ModelConfigSnapshot {
const out: Partial<ModelConfigSnapshot> = {};
const env = readEnvFile();
const envLookup = (key: string): string | undefined => {
const fromFile = pickEnvValue(env, key);
if (fromFile !== undefined) return fromFile;
return process.env[key];
};
const out: Partial<ModelConfigSnapshot> = {
agentTypes: {
owner: agentTypeForRole('owner', envLookup) ?? 'codex',
reviewer: agentTypeForRole('reviewer', envLookup) ?? 'claude-code',
arbiter: agentTypeForRole('arbiter', envLookup),
},
};
for (const role of ROLE_KEYS) {
const model = readEnvOrProcess(`${role}_MODEL`) ?? '';
const effort = readEnvOrProcess(`${role}_EFFORT`) ?? '';
@@ -308,6 +333,8 @@ export function updateModelConfig(
if (fs.existsSync(file)) {
content = fs.readFileSync(file, 'utf-8');
}
const envLookup = (key: string): string | undefined =>
pickEnvValue(content, key) ?? process.env[key];
for (const role of ROLE_KEYS) {
const update = (
@@ -318,6 +345,19 @@ export function updateModelConfig(
content = setOrInsertEnvLine(content, `${role}_MODEL`, update.model);
}
if (update.effort !== undefined) {
const agentType = agentTypeForRole(
role.toLowerCase() as 'owner' | 'reviewer' | 'arbiter',
envLookup,
);
if (
update.effort &&
agentType &&
!isEffortSupported(agentType, update.effort)
) {
throw new Error(
`${role}_EFFORT=${update.effort} is not supported for ${agentType} agents`,
);
}
content = setOrInsertEnvLine(content, `${role}_EFFORT`, update.effort);
}
}
@@ -564,7 +604,7 @@ export function stopCodexAccountRefreshLoop(): void {
}
}
function codexConfigPath(): string {
function codexConfigFile(): string {
return path.join(settingsHomeDir(), '.codex', 'config.toml');
}
@@ -573,51 +613,15 @@ function claudeSettingsPath(): string {
}
function readCodexFastMode(): boolean {
const file = codexConfigPath();
if (!fs.existsSync(file)) return false;
const content = fs.readFileSync(file, 'utf-8');
// [features] section, look for fast_mode = true|false
const featuresMatch = content.match(/\[features\][\s\S]*?(?=^\[|$)/m);
const block = featuresMatch ? featuresMatch[0] : content;
const m = block.match(/^\s*fast_mode\s*=\s*(true|false)\s*$/m);
return m ? m[1] === 'true' : false;
return readCodexFeatureFromFile(codexConfigFile(), 'fast_mode');
}
function writeCodexFastMode(value: boolean): void {
const file = codexConfigPath();
let content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
if (/^\s*fast_mode\s*=\s*(true|false)\s*$/m.test(content)) {
content = content.replace(
/^\s*fast_mode\s*=\s*(true|false)\s*$/m,
`fast_mode = ${value}`,
);
} else if (/^\[features\]/m.test(content)) {
content = content.replace(
/^\[features\]\s*$/m,
`[features]\nfast_mode = ${value}`,
);
} else {
const trimmed = content.replace(/\s*$/, '');
content = `${trimmed}\n\n[features]\nfast_mode = ${value}\n`;
}
const tempPath = `${file}.tmp`;
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(tempPath, content, { mode: 0o600 });
fs.renameSync(tempPath, file);
writeCodexFeatureToFile(codexConfigFile(), 'fast_mode', value);
}
function readClaudeFastMode(): boolean {
const file = claudeSettingsPath();
if (!fs.existsSync(file)) return false;
try {
const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<
string,
unknown
>;
return data.fastMode === true;
} catch {
return false;
}
return readHostClaudeFastMode();
}
function writeClaudeFastMode(value: boolean): void {
@@ -658,19 +662,26 @@ export function updateFastMode(
}
function readCodexGoals(): boolean {
const fromConfig = readCodexFeatureFromFile(codexConfigFile(), 'goals');
if (fromConfig) return true;
// Legacy .env opt-in kept for backward compatibility until migrated.
return readEnvOrProcess('CODEX_GOALS') === 'true';
}
function writeCodexGoals(value: boolean): void {
writeCodexFeatureToFile(codexConfigFile(), 'goals', value);
const file = envFilePath();
const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
const updated = setOrInsertEnvLine(
content,
'CODEX_GOALS',
value ? 'true' : 'false',
);
if (!fs.existsSync(file)) return;
const content = fs.readFileSync(file, 'utf-8');
if (!/^CODEX_GOALS=.*$/m.test(content)) return;
const updated = content
.split('\n')
.filter((line) => !/^CODEX_GOALS=.*$/.test(line))
.join('\n')
.replace(/\n+$/, '');
const tempPath = `${file}.tmp`;
fs.writeFileSync(tempPath, updated, { mode: 0o600 });
fs.writeFileSync(tempPath, updated ? `${updated}\n` : '', { mode: 0o600 });
fs.renameSync(tempPath, file);
}

View File

@@ -42,6 +42,11 @@ const modelConfig: ModelConfigSnapshot = {
owner: { model: 'gpt-5.4', effort: 'medium' },
reviewer: { model: 'claude-sonnet', effort: 'high' },
arbiter: { model: 'gpt-5.4', effort: 'high' },
agentTypes: {
owner: 'codex',
reviewer: 'claude-code',
arbiter: null,
},
};
const fastMode: FastModeSnapshot = { codex: true, claude: false };

View File

@@ -149,7 +149,9 @@ async function handleModelSettingsRoute(
try {
return jsonResponse(deps.updateModelConfig(body));
} catch (err) {
return jsonResponse({ error: errorMessage(err) }, { status: 500 });
const message = errorMessage(err);
const status = message.includes('not supported') ? 400 : 500;
return jsonResponse({ error: message }, { status });
}
}