Skip to content
Closed
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
3 changes: 3 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

### Added
- The model selector now prompts for a reasoning level when assigning any reasoning-capable model to the DEFAULT target, not just OpenAI/OpenAI-Codex models. Selecting e.g. an Anthropic reasoning model for DEFAULT opens the same "Reasoning for Default" menu as role-agent assignments and persists the choice as an explicit `:level` suffix in `modelRoles.default`, so the pinned effort survives restarts instead of silently inheriting `defaultThinkingLevel`. Non-reasoning models and temporary model switches are unaffected.

## [0.13.1] - 2026-08-11

### Added
Expand Down
7 changes: 6 additions & 1 deletion packages/coding-agent/src/modes/components/model-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2435,7 +2435,12 @@ export class ModelSelectorComponent extends Container {
function requiresExplicitThinkingChoice(model: Model, role: GjcModelAssignmentTargetId | null): boolean {
if (model.reasoning !== true) return false;
if (model.provider === "openai" || model.provider === "openai-codex") return true;
return role !== null && GJC_MODEL_ASSIGNMENT_TARGETS[role].settingsPath === "task.agentModelOverrides";
if (role === null) return false;
// DEFAULT assignments prompt for a reasoning level regardless of provider so
// non-OpenAI reasoning models (e.g. Anthropic) can pin an explicit effort in
// modelRoles.default instead of silently inheriting defaultThinkingLevel.
if (role === "default") return true;
return GJC_MODEL_ASSIGNMENT_TARGETS[role].settingsPath === "task.agentModelOverrides";
}

function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ function createSelector(

/**
* Reasoning model whose provider alone does NOT force an explicit thinking
* choice for the DEFAULT target (unlike openai/openai-codex), mirroring
* Anthropic reasoning models such as claude-fable-5. Role-agent targets
* (task.agentModelOverrides) still require an explicit choice, so batch
* assignment must surface the reasoning menu.
* choice (unlike openai/openai-codex), mirroring Anthropic reasoning models
* such as claude-fable-5. DEFAULT and role-agent targets both require an
* explicit choice, so single and batch assignment must surface the
* reasoning menu.
*/
function createAnthropicReasoningModel(id: string): Model {
return {
Expand Down
102 changes: 101 additions & 1 deletion packages/coding-agent/test/model-selector-role-badge-thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ function createOpenAIModel(provider: "openai" | "openai-codex", id: string, reas
};
}

function createAnthropicReasoningModel(id: string): Model {
return {
id,
name: id,
api: "anthropic-messages",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
reasoning: true,
thinking: {
minLevel: Effort.Low,
maxLevel: Effort.XHigh,
mode: "anthropic-adaptive",
},
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 64000,
} as Model;
}

function createOllamaCloudModel(id: string): Model {
return {
id,
Expand Down Expand Up @@ -191,12 +211,19 @@ describe("ModelSelector canonical model selection", () => {
expect(actionRendered).not.toContain("Set as SMOL");
expect(actionRendered).not.toContain("Set as TASK");

selector.handleInput("\n");
// Reasoning-capable model on the DEFAULT target: the reasoning menu opens,
// seeded from the existing DEFAULT (low) binding.
expect(selected).toBeUndefined();
const thinkingRendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(thinkingRendered).toContain("Reasoning for Default: low");

selector.handleInput("\n");
const selectedAfterEnter = selected;
if (!selectedAfterEnter) throw new Error("Expected Enter to select a model");
expect(selectedAfterEnter.model).toBe(model);
expect(selectedAfterEnter.role).toBe("default");
expect(selectedAfterEnter.thinkingLevel).toBe(ThinkingLevel.Off);
expect(selectedAfterEnter.thinkingLevel).toBe(ThinkingLevel.Low);
expect(selectedAfterEnter.selector).toBe(`${model.provider}/${model.id}`);
});

Expand Down Expand Up @@ -525,6 +552,79 @@ describe("ModelSelector canonical model selection", () => {
expect(selectedAfterThinking.selector).toBe(`${model.provider}/${model.id}`);
});

test("prompts for reasoning before assigning Anthropic reasoning default models", async () => {
installTestTheme();
const model = createAnthropicReasoningModel("claude-fable-5");
const settings = Settings.isolated({});

let selected: SelectionCapture | undefined;
const selector = createSelector(
model,
settings,
selection => {
if (selection.kind === "assignment") selected = selection;
},
{ thinkingLevel: null },
);
await Bun.sleep(0);
installTestTheme();

selector.handleInput("\n");
selector.handleInput("\n");

// The reasoning menu must open instead of committing the assignment.
expect(selected).toBeUndefined();
const thinkingRendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(thinkingRendered).toContain("Reasoning for Default: off");
expect(thinkingRendered).toContain("xhigh");

// Levels are [off, low, medium, high, xhigh]; pick xhigh.
for (let i = 0; i < 4; i++) selector.handleInput("\x1b[B");
const afterNav = normalizeRenderedText(selector.render(220).join("\n"));
expect(afterNav).toContain("Reasoning for Default: xhigh");
selector.handleInput("\n");

const selectedAfterThinking = selected;
if (!selectedAfterThinking) throw new Error("Expected Anthropic selection after reasoning choice");
expect(selectedAfterThinking.model).toBe(model);
expect(selectedAfterThinking.role).toBe("default");
expect(selectedAfterThinking.thinkingLevel).toBe(ThinkingLevel.XHigh);
expect(selectedAfterThinking.selector).toBe(`${model.provider}/${model.id}`);
});

test("does not prompt when assigning Anthropic non-reasoning models to default", async () => {
installTestTheme();
const reasoningModel = createAnthropicReasoningModel("claude-plain-base");
const model = {
...reasoningModel,
id: "claude-plain",
name: "claude-plain",
reasoning: false,
thinking: undefined,
} as Model;
const settings = Settings.isolated({});

let selected: SelectionCapture | undefined;
const selector = createSelector(
model,
settings,
selection => {
if (selection.kind === "assignment") selected = selection;
},
{ thinkingLevel: null },
);
await Bun.sleep(0);
installTestTheme();

selector.handleInput("\n");
selector.handleInput("\n");

const selectedDirect = selected;
if (!selectedDirect) throw new Error("Expected direct non-reasoning selection");
expect(selectedDirect.role).toBe("default");
expect(selectedDirect.selector).toBe(`${model.provider}/${model.id}`);
});

test("can explicitly choose off for OpenAI reasoning default models", async () => {
installTestTheme();
const model = createOpenAIModel("openai", "gpt-reasoning-off-test");
Expand Down
Loading