Skip to content

feat: add model profiles for saving and switching model configurations#5253

Open
oldschoola wants to merge 10 commits into
can1357:mainfrom
oldschoola:profiles
Open

feat: add model profiles for saving and switching model configurations#5253
oldschoola wants to merge 10 commits into
can1357:mainfrom
oldschoola:profiles

Conversation

@oldschoola

Copy link
Copy Markdown
Contributor

Summary

Add the ability to save and switch between named "profiles" that capture the current model configuration (model selections per role + thinking level).

Changes

New files

  • src/config/profiles.tssaveProfile, switchProfile, deleteProfile, getProfiles, getProfileNames helpers. Pure settings operations, no session dependency.

Modified files

  • src/config/settings-schema.tsProfileSnapshot interface (modelRoles + defaultThinkingLevel), profiles record schema entry, GroupTypeMap entry
  • src/slash-commands/builtin-registry.ts/profiles slash command with save, switch, delete, list subcommands (both ACP handle and TUI handleTui); uses resolveModelRoleValue for model selector resolution on switch
  • src/modes/components/model-hub.tsonSaveProfile callback, profileName strip variant, s keybinding in roles view to save current config as a named profile
  • src/modes/controllers/selector-controller.ts — wires onSaveProfile callback to saveProfile()
  • CHANGELOG.md — entry under [Unreleased]Added

Usage

/profiles slash command

/profiles save my-setup      # Save current model config as "my-setup"
/profiles switch my-setup    # Switch to "my-setup" (replaces all role assignments)
/profiles delete my-setup    # Delete "my-setup"
/profiles list               # List all saved profiles

/models roles view

Press s in the roles view to open a footer name input — type a name and press Enter to save the current model configuration as a profile.

Key design decisions

  • Clean cutover on switch: switchProfile uses settings.set("modelRoles", profile.modelRoles) to replace the ENTIRE record — not a loop of setModelRole() per entry. This ensures stale role assignments from the previously-active profile are removed.
  • Model selector resolution: Profile switch uses resolveModelRoleValue() from model-resolver.ts — not manual split("/")/split(":") — to handle multi-slash provider IDs (e.g. OpenRouter passthrough) and thinking-level suffixes correctly.
  • Profile snapshot: Captures modelRoles (all role → selector assignments) + defaultThinkingLevel. These together represent "what model is selected for each role."
  • No settings UI: The profiles setting has no ui metadata — managed via the slash command and model hub keybinding, same pattern as modelRoles.

Verification

  • Type check: bun run check:types — PASS
  • Biome: Clean on all modified files
  • Smoke test: save/switch/delete/list all verified; non-existent profile switch/delete returns false; overwrite works correctly

Test plan

  • cd packages/coding-agent && bun run check:types
  • Open /models, press s, type a profile name, Enter — verify "Profile saved" status
  • /profiles list — verify the profile appears
  • Change model assignments, then /profiles switch <name> — verify all roles revert
  • /profiles delete <name> — verify it disappears from list

- Add /profiles slash command with save, switch, delete, list subcommands
- Add 's' keybinding in /models roles view to save current config as a
  named profile via a footer name input strip
- Profiles capture modelRoles (all role → model selector assignments)
  and defaultThinkingLevel as a ProfileSnapshot
- switchProfile uses clean cutover: settings.set('modelRoles', snapshot)
  replaces the entire record, not a per-role merge — stale assignments
  from the previous profile are removed
- Profile switch resolves the default role's model via
  resolveModelRoleValue (handles multi-slash IDs, thinking suffixes)
- Add profiles record to settings schema with ProfileSnapshot type
@github-actions github-actions Bot added the vouched Passed the vouch gate label Jul 12, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf1aab6d5e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/coding-agent/src/slash-commands/builtin-registry.ts Outdated
Comment thread packages/coding-agent/src/config/profiles.ts Outdated
- Apply resolved thinking level to session on profile switch (not just
  settings) so the next turn uses the profile's reasoning level
- Clone the profiles record before mutating in saveProfile/deleteProfile
  to avoid mutating the shared schema default object

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1eb7b6e692

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const profiles = getProfiles(settings);
const profile = profiles[name];
if (!profile) return false;
settings.set("modelRoles", profile.modelRoles);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply profile switches to runtime overrides

When a session was started with --model or role env overrides, those assignments are stored as runtime modelRoles overrides (main.ts:869, main.ts:1099), and Settings merges overrides after globals (settings.ts:1378). This line only writes the global modelRoles, so /profiles switch foo leaves the override winning; the handler then resolves the old overridden role and reports the profile as switched even though the effective role/model did not change. Replace or clear the runtime model-role override as part of switching profiles.

Useful? React with 👍 / 👎.

The 's' key conflicted with search input in the model hub. Use ctrl+s
instead so typing in the search bar doesn't trigger the save dialog.
- /profiles with no args now opens an interactive picker (SelectList)
  to browse and select a profile to switch to
- Extract switchProfileAndResolve() as the single source of truth for
  profile switching — eliminates duplication across ACP handler, TUI
  handler, and the new interactive picker
- Add showProfileSelector to InteractiveModeContext and implement in
  selector-controller using the showSelector editor-slot pattern
Container doesn't forward input to child components, so Escape/cancel
was never reaching the SelectList. Add handleInput delegation so the
picker can be dismissed when empty.
Container doesn't forward handleInput to child components, so arrows,
Enter, and Escape never reached the SelectList. Return getSelectList()
as the focus target, matching the thinking/theme/plugin selector pattern.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7d628c826

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/coding-agent/src/modes/controllers/selector-controller.ts Outdated
…picker

- switchProfile now clears runtime modelRoles overrides (from --model
  or env) so the profile's assignments take effect instead of the
  override winning
- showProfileSelector picker now awaits setModel before applying
  thinking level, preventing the async model switch from overwriting
  the profile's thinking level with the model's default

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d18f5d707

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/coding-agent/src/config/profiles.ts Outdated
getProfiles() reads the merged view (global + project + overrides), but
settings.set writes to global. saveProfile and deleteProfile now read
from the global layer only via settings.getGlobalProfiles(), preventing
project-scoped profiles from leaking into the global config when saving
in a project context.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 474592b41d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/coding-agent/src/config/profiles.ts Outdated
…fault

When a profile has no modelRoles.default (saved while default was
automatic), switchProfileAndResolve now signals needsReset. The
callers reset the session to its automatic/default model instead of
leaving the previous profile's model active.

Added AgentSession.resetToDefaultModel() which mirrors startup
resolution: filters available models to those with configured auth,
then picks via pickDefaultAvailableModel.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f361e3d77

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/coding-agent/src/config/profiles.ts
When a profile has a non-empty modelRoles.default that doesn't resolve
against available models (provider disabled, credentials removed, typo),
switchProfileAndResolve now signals needsReset + unresolvedDefault.
Callers warn the user and fall back to automatic default selection
instead of silently keeping the previous model.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4056bac275

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/coding-agent/src/config/profiles.ts
@roboomp roboomp added cli CLI commands and arguments feat review:p2 triaged tui Terminal UI rendering and display ux User experience improvements labels Jul 14, 2026

@roboomp roboomp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review:p2 — coherent user-visible feature, but the current switch path has blocking contract failures.
Blocking: effective roles are deep-merged rather than replaced; saved default thinking is not applied live; inherited profile keys can wipe roles; live transition failures are reported as success.
Maintainer call: confirm whether selecting a profile must override project/config role layers; the advertised clean cutover requires an exact effective-layer policy.
Thanks @oldschoola for the focused feature and regression groundwork.

const profiles = getProfiles(settings);
const profile = profiles[name];
if (!profile) return false;
settings.set("modelRoles", profile.modelRoles);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: This does not replace the effective role record when project/config layers exist. Settings.set() writes only #global, then #rebuildMerged() recursively merges #project and #configOverlay over it (settings.ts:1375-1411). A project-scoped role omitted by the profile therefore survives, and a project-scoped default can override the profile entirely, contradicting the advertised clean cutover. Please apply the snapshot through an exact effective layer (or otherwise mask higher-layer keys) and cover a switch from a project-scoped stale role.

return {
ok: true,
model: resolved.model,
thinkingLevel: resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: The snapshot's defaultThinkingLevel is persisted above, but this drops it from the live switch whenever the default selector has no explicit :level. AgentSession.setModel() re-applies the target model's defaultLevel or preserves the current level (agent-session.ts:9071-9073); it does not read the newly written setting, and the reset branch has the same behavior. A profile saved with an unsuffixed model and defaultThinkingLevel: "low" can therefore switch models while keeping the old/model-default effort. Return/apply the snapshot's configured level here (including auto) and add a session-level regression test.

*/
export function switchProfile(settings: Settings, name: string): boolean {
const profiles = getProfiles(settings);
const profile = profiles[name];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: This lookup accepts inherited object keys as profiles. With the normal {} prototype, /profiles switch toString finds Object.prototype.toString, passes the truthiness guard, writes undefined as modelRoles, clears the runtime override, and reports success even though no such profile exists. deleteProfile() has the same issue via name in profiles. Require Object.hasOwn(profiles, name) and validate the snapshot shape before applying it; please cover prototype-key names in the missing-profile tests.

Comment on lines +441 to +452
runtime.session.setThinkingLevel(result.thinkingLevel);
}
} catch {
// Settings applied, model switch failed
}
}
if (result.needsReset) {
try {
await runtime.session.resetToDefaultModel?.();
} catch {
// Non-critical — settings are still applied
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Both live-transition failures are swallowed, after which this handler prints Switched to profile and emits config/title changes. If setModel() rejects (auth/metadata/sync failure), or automatic reset cannot find an authenticated model, persisted roles change while the session keeps its old model and the command claims success. The TUI branch (:525, :534) and interactive picker (selector-controller.ts:1495, :1504) repeat the same false-success path. Surface a failed outcome and do not report a completed switch unless the live transition succeeded.

const resolved = resolveModelRoleValue(defaultRole, availableModels as Model[], {
settings,
});
if (!resolved.model) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix: This assertion discards the ReadonlyArray contract and violates the repository's no-as rule. resolveModelRoleValue() does not mutate this collection, so its parameter should accept readonly Model[]; alternatively keep this API mutable to match the existing resolver instead of asserting away the mismatch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI commands and arguments feat review:p2 triaged tui Terminal UI rendering and display ux User experience improvements vouched Passed the vouch gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants