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
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions packages/coding-agent/src/modes/components/model-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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;
}
Expand Down
69 changes: 48 additions & 21 deletions packages/coding-agent/test/model-selector-profiles-redteam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading