diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 09f98270e3..37a721cfe8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,10 @@ - The built-in `claude-opus`, `opus-codex`, and `fable-opus-codex` presets now use `anthropic/claude-opus-5` instead of `anthropic/claude-opus-4-8`, with effort suffixes preserved; `packages/ai/src/models.json` was regenerated so `anthropic/claude-opus-5` resolves; non-opus roles (`anthropic/claude-sonnet-5` executor/planner overrides, codex and fable roles) are unchanged. +### Changed + +- `/model` preset selection now offers `Set as default` as the first action while retaining `Apply for this session`, custom preset rename, and delete actions. + ### Fixed - Restricted role-agent `bash` now accepts literal mid-word tildes, so git revision syntax such as `git diff HEAD~1` no longer has to be quoted. Bash performs tilde expansion only at the start of a word, so word-initial forms (`~`, `~/path`, `~user`) remain blocked. diff --git a/packages/coding-agent/src/modes/components/model-selector.ts b/packages/coding-agent/src/modes/components/model-selector.ts index bf17833c90..ffdc0ba552 100644 --- a/packages/coding-agent/src/modes/components/model-selector.ts +++ b/packages/coding-agent/src/modes/components/model-selector.ts @@ -229,8 +229,8 @@ const PROFILE_ROLE_PREVIEW_ORDER: GjcModelAssignmentTargetId[] = [ "critic", "architect", ]; -const PRESET_SCOPE_LABELS = ["Apply for this session", "Set as default"]; -const CUSTOM_PRESET_SCOPE_LABELS = ["Apply for this session", "Set as default", "Rename", "Delete"]; +const PRESET_SCOPE_LABELS = ["Set as default", "Apply for this session"]; +const CUSTOM_PRESET_SCOPE_LABELS = ["Set as default", "Apply for this session", "Rename", "Delete"]; function isPrintableCharacter(keyData: string): boolean { return keyData.length === 1 && keyData >= " " && keyData !== "\x7f"; @@ -1246,7 +1246,7 @@ export class ModelSelectorComponent extends Container { ); } } else { - this.#listContainer.addChild(new Text(theme.fg("muted", " Press Enter to apply this preset"), 0, 0)); + this.#listContainer.addChild(new Text(theme.fg("muted", " Press Enter to choose an action"), 0, 0)); } } @@ -1655,7 +1655,7 @@ export class ModelSelectorComponent extends Container { this.#onSelectCallback({ kind: "profile", profileName: this.#previewProfileName, - setDefault: this.#presetScopeIndex === 1, + setDefault: this.#presetScopeIndex === 0, }); return; } diff --git a/packages/coding-agent/test/model-selector-profiles-redteam.test.ts b/packages/coding-agent/test/model-selector-profiles-redteam.test.ts index 9599b07d35..8872ae78ed 100644 --- a/packages/coding-agent/test/model-selector-profiles-redteam.test.ts +++ b/packages/coding-agent/test/model-selector-profiles-redteam.test.ts @@ -178,35 +178,62 @@ describe("model selector profile red-team", () => { expect(rendered.match(/Profile Alpha/g) ?? []).toHaveLength(1); }); - test("profile actions wire Apply for this session to persistDefault false and Set as default to true", async () => { + test("profile actions default to persistence and retain session-only application", async () => { const selections: ModelSelectorSelection[] = []; - const applySelector = createSelector(selection => { + const select = (selection: ModelSelectorSelection) => { selections.push(selection); - }); - await renderSelector(applySelector); - applySelector.handleInput("\x1b[C"); - applySelector.handleInput("\x1b[B"); - applySelector.handleInput("\n"); - applySelector.handleInput("\n"); - applySelector.handleInput("\n"); - - const defaultSelector = createSelector(selection => { - selections.push(selection); - }); - await renderSelector(defaultSelector); - defaultSelector.handleInput("\x1b[C"); - defaultSelector.handleInput("\x1b[B"); - defaultSelector.handleInput("\n"); - defaultSelector.handleInput("\n"); - defaultSelector.handleInput("\x1b[B"); - defaultSelector.handleInput("\n"); + }; + const persistentSelector = createSelector(select); + await renderSelector(persistentSelector); + persistentSelector.handleInput("\x1b[C"); + persistentSelector.handleInput("\x1b[B"); + persistentSelector.handleInput("\n"); + persistentSelector.handleInput("\n"); + + const menu = normalizeRenderedText(persistentSelector.render(240).join("\n")); + expect(menu).toContain("Set as default"); + expect(menu).toContain("Apply for this session"); + persistentSelector.handleInput("\n"); + + const sessionSelector = createSelector(select); + await renderSelector(sessionSelector); + sessionSelector.handleInput("\x1b[C"); + sessionSelector.handleInput("\x1b[B"); + sessionSelector.handleInput("\n"); + sessionSelector.handleInput("\n"); + sessionSelector.handleInput("\x1b[B"); + sessionSelector.handleInput("\n"); expect(selections).toEqual([ - { kind: "profile", profileName: "profile-a", setDefault: false }, { kind: "profile", profileName: "profile-a", setDefault: true }, + { kind: "profile", profileName: "profile-a", setDefault: false }, ]); }); + test("custom profile action indices retain rename and delete", async () => { + const renamed: ModelSelectorSelection[] = []; + const renameSelector = createSelector(selection => renamed.push(selection)); + await renderSelector(renameSelector); + renameSelector.refreshPresetProfiles("profile-a"); + renameSelector.handleInput("\n"); + renameSelector.handleInput("\x1b[B"); + renameSelector.handleInput("\x1b[B"); + renameSelector.handleInput("\n"); + + const deleted: ModelSelectorSelection[] = []; + const deleteSelector = createSelector(selection => deleted.push(selection)); + await renderSelector(deleteSelector); + deleteSelector.refreshPresetProfiles("profile-a"); + deleteSelector.handleInput("\n"); + deleteSelector.handleInput("\x1b[B"); + deleteSelector.handleInput("\x1b[B"); + deleteSelector.handleInput("\x1b[B"); + deleteSelector.handleInput("\n"); + + expect(renamed).toEqual([{ kind: "renameProfile", profileName: "profile-a" }]); + expect(deleted).toEqual([{ kind: "deleteProfile", profileName: "profile-a" }]); + }); + test("controller persists only Set as default and leaves Apply for this session non-default", async () => { const sessionOnly = createControllerContext(); await selectProfileThroughController(new SelectorController(sessionOnly.ctx as never), false);