Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/backends/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,43 @@ export const CLAUDE_CODE_ENGINE_DEFINITION: AgentEngineDefinition = {
options: CLAUDE_CODE_MODELS,
},
logLabel: 'Claude Code Log',
settings: {
title: 'Claude Code Settings',
description: 'Effort level and thinking mode for Claude Code runs.',
fields: [
{
key: 'effort',
label: 'Effort',
type: 'select',
description: 'Controls the overall effort level applied during the run.',
options: [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'max', label: 'Max' },
],
},
{
key: 'thinking',
label: 'Thinking',
type: 'select',
description: 'Controls extended thinking mode.',
options: [
{ value: 'adaptive', label: 'Adaptive' },
{ value: 'enabled', label: 'Enabled' },
{ value: 'disabled', label: 'Disabled' },
],
},
{
key: 'thinkingBudgetTokens',
label: 'Thinking Budget Tokens',
// TODO: Frontend 'number' field type is not yet supported (Story #2).
// The dashboard will render this field once numeric fields are implemented.
type: 'number',
description: 'Maximum tokens allocated for extended thinking (optional).',
},
],
},
};

export const CODEX_ENGINE_DEFINITION: AgentEngineDefinition = {
Expand Down
5 changes: 5 additions & 0 deletions src/backends/claude-code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { AgentEngine, AgentEngineResult, AgentExecutionPlan } from '../type
import { buildClaudeEnv } from './env.js';
import { buildHooks } from './hooks.js';
import { CLAUDE_CODE_MODEL_IDS, DEFAULT_CLAUDE_CODE_MODEL } from './models.js';
import { ClaudeCodeSettingsSchema } from './settings.js';

export { buildToolGuidance, buildTaskPrompt, buildSystemPrompt } from '../nativeTools.js';
export { buildClaudeEnv as buildEnv } from './env.js';
Expand Down Expand Up @@ -460,6 +461,10 @@ export class ClaudeCodeEngine implements AgentEngine {
return resolveClaudeModel(cascadeModel);
}

getSettingsSchema() {
return ClaudeCodeSettingsSchema;
}

async beforeExecute(plan: AgentExecutionPlan): Promise<void> {
// Ensure onboarding flag exists (required for both API key and subscription auth)
ensureOnboardingFlag();
Expand Down
31 changes: 31 additions & 0 deletions src/backends/claude-code/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from 'zod';
import { getEngineSettings } from '../../config/engineSettings.js';
import type { ProjectConfig } from '../../types/index.js';

export const ClaudeCodeSettingsSchema = z.object({
effort: z.enum(['low', 'medium', 'high', 'max']).optional(),
thinking: z.enum(['adaptive', 'enabled', 'disabled']).optional(),
// TODO: Frontend 'number' field type is not yet supported (Story #2).
// This field is defined here for catalog registration; the dashboard will
// render it once numeric fields are implemented.
thinkingBudgetTokens: z.number().int().positive().optional(),
});

export type ClaudeCodeSettings = z.infer<typeof ClaudeCodeSettingsSchema>;

export interface ResolvedClaudeCodeSettings {
effort: NonNullable<ClaudeCodeSettings['effort']>;
thinking: NonNullable<ClaudeCodeSettings['thinking']>;
thinkingBudgetTokens?: ClaudeCodeSettings['thinkingBudgetTokens'];
}

export function resolveClaudeCodeSettings(project: ProjectConfig): ResolvedClaudeCodeSettings {
const claudeCode =
getEngineSettings(project.engineSettings, 'claude-code', ClaudeCodeSettingsSchema) ?? {};

return {
effort: claudeCode.effort ?? 'high',
thinking: claudeCode.thinking ?? 'adaptive',
thinkingBudgetTokens: claudeCode.thinkingBudgetTokens,
};
}
9 changes: 9 additions & 0 deletions src/backends/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ export type AgentEngineSettingField =
label: string;
type: 'boolean';
description?: string;
}
| {
key: string;
label: string;
// TODO: Frontend rendering for 'number' fields is not yet implemented (Story #2).
// The catalog definition is registered here; the dashboard will render it once
// numeric field support is added.
type: 'number';
description?: string;
};

export interface AgentEngineSettingsDefinition {
Expand Down
2 changes: 2 additions & 0 deletions src/config/engineSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { z } from 'zod';

// Re-export schemas from engine directories for backward compatibility.
export { ClaudeCodeSettingsSchema } from '../backends/claude-code/settings.js';
export type { ClaudeCodeSettings } from '../backends/claude-code/settings.js';
export { CodexSettingsSchema } from '../backends/codex/settings.js';
export type { CodexSettings } from '../backends/codex/settings.js';
export { OpenCodeSettingsSchema } from '../backends/opencode/settings.js';
Expand Down
78 changes: 78 additions & 0 deletions tests/unit/backends/claude-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
CLAUDE_CODE_MODEL_IDS,
DEFAULT_CLAUDE_CODE_MODEL,
} from '../../../src/backends/claude-code/models.js';
import { resolveClaudeCodeSettings } from '../../../src/backends/claude-code/settings.js';
import type { AgentExecutionPlan, ToolManifest } from '../../../src/backends/types.js';

const mockQuery = vi.mocked(query);
Expand Down Expand Up @@ -1390,3 +1391,80 @@ describe('ClaudeCodeEngine lifecycle hooks', () => {
expect(existsSync(sessionDir)).toBe(false);
});
});

describe('resolveClaudeCodeSettings', () => {
it('returns defaults when no engine settings are configured', () => {
const project = makeInput().project;
expect(resolveClaudeCodeSettings(project)).toEqual({
effort: 'high',
thinking: 'adaptive',
thinkingBudgetTokens: undefined,
});
});

it('applies explicit effort modes from project engine settings', () => {
const project = {
...makeInput().project,
engineSettings: { 'claude-code': { effort: 'max' } },
} as AgentExecutionPlan['project'];
expect(resolveClaudeCodeSettings(project)).toEqual({
effort: 'max',
thinking: 'adaptive',
thinkingBudgetTokens: undefined,
});

const projectLow = {
...makeInput().project,
engineSettings: { 'claude-code': { effort: 'low' } },
} as AgentExecutionPlan['project'];
expect(resolveClaudeCodeSettings(projectLow).effort).toBe('low');

const projectMedium = {
...makeInput().project,
engineSettings: { 'claude-code': { effort: 'medium' } },
} as AgentExecutionPlan['project'];
expect(resolveClaudeCodeSettings(projectMedium).effort).toBe('medium');
});

it('applies explicit thinking modes from project engine settings', () => {
const projectEnabled = {
...makeInput().project,
engineSettings: { 'claude-code': { thinking: 'enabled' } },
} as AgentExecutionPlan['project'];
expect(resolveClaudeCodeSettings(projectEnabled)).toEqual({
effort: 'high',
thinking: 'enabled',
thinkingBudgetTokens: undefined,
});

const projectDisabled = {
...makeInput().project,
engineSettings: { 'claude-code': { thinking: 'disabled' } },
} as AgentExecutionPlan['project'];
expect(resolveClaudeCodeSettings(projectDisabled).thinking).toBe('disabled');
});

it('applies thinkingBudgetTokens when provided', () => {
const project = {
...makeInput().project,
engineSettings: { 'claude-code': { thinkingBudgetTokens: 10000 } },
} as AgentExecutionPlan['project'];
expect(resolveClaudeCodeSettings(project)).toEqual({
effort: 'high',
thinking: 'adaptive',
thinkingBudgetTokens: 10000,
});
});

it('ClaudeCodeEngine.getSettingsSchema() returns ClaudeCodeSettingsSchema', () => {
const engine = new ClaudeCodeEngine();
const schema = engine.getSettingsSchema();
expect(schema).toBeDefined();
// Verify it parses valid settings
const result = schema.safeParse({ effort: 'high', thinking: 'adaptive' });
expect(result.success).toBe(true);
// Verify it rejects invalid settings
const bad = schema.safeParse({ effort: 'ultra' });
expect(bad.success).toBe(false);
});
});
2 changes: 1 addition & 1 deletion tests/unit/config/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ describe.concurrent('validateConfig', () => {
repo: 'owner/repo',
trello: { boardId: 'b1', lists: {}, labels: {} },
engineSettings: {
'claude-code': {
'unknown-engine': {
foo: 'bar',
},
},
Expand Down
12 changes: 12 additions & 0 deletions web/src/components/settings/engine-settings-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ type EngineSettingField =
label: string;
type: 'boolean';
description?: string;
}
| {
key: string;
label: string;
// TODO: Frontend rendering for 'number' fields is not yet implemented (Story #2).
// The field type is defined here for type compatibility with the backend catalog;
// the dashboard will render it once numeric field support is added.
type: 'number';
description?: string;
};

interface EngineDefinition {
Expand Down Expand Up @@ -97,6 +106,9 @@ export function EngineSettingsFields({

<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{engine.settings.fields.map((field) => {
// TODO: 'number' field rendering is not yet implemented (Story #2).
if (field.type === 'number') return null;

const rawValue = activeEngineValues[field.key];

return (
Expand Down
Loading