Skip to content

Commit 191e5bd

Browse files
TylerBarnesMastra Code (openai/gpt-5.3-codex)
andauthored
fix: unify /models pack flow and improve custom pack editing UX (#13512)
This updates model pack management to use /models as the single entry point and fixes rough edges in custom pack editing. Before: /models and /models:pack were split, and editing a custom pack forced full reselection. After: /models opens pack selection directly, and custom pack edit is targeted: Rename -> <name> plan -> <model> build -> <model> fast -> <model> Save Saving edits no longer auto-activates the pack. Thread pack state restores per thread, model use counts are tracked again for sorting, and custom pack rename/delete persistence avoids stale entries. <img width="722" height="142" alt="Screenshot 2026-02-25 at 3 00 15 PM" src="https://github.com/user-attachments/assets/5ba3bf77-95ab-4269-bf40-420e3dcf0fa1" /> then you see this view <img width="701" height="391" alt="Screenshot 2026-02-25 at 2 58 34 PM" src="https://github.com/user-attachments/assets/84efcf42-5d23-44db-abad-0b993276ec05" /> selecting one gives you options for it <img width="609" height="326" alt="Screenshot 2026-02-25 at 2 58 39 PM" src="https://github.com/user-attachments/assets/7bc8fb31-4c51-4481-8acb-720a5f4d3186" /> when you hit edit you get to pick which mode to update or you can rename it <img width="685" height="277" alt="Screenshot 2026-02-25 at 2 58 44 PM" src="https://github.com/user-attachments/assets/f42a3a0f-971c-48ec-bed9-1657a7771e74" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Thread-aware model pack restore; pack choice persists per thread. * Named custom model packs with full create/rename/edit/delete flows and inline naming UI. * Model picker learns recent selections and persists usage-based ordering. * **Improvements** * Consolidated pack management into a single /models command; help updated and legacy /models:pack removed. * Migrated legacy "Varied" into a saved custom pack and simplified multi-provider discovery. * **Tests & Verification** * Added unit tests and a manual verification checklist covering the above. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Mastra Code (openai/gpt-5.3-codex) <noreply@mastra.ai>
1 parent a96cecc commit 191e5bd

22 files changed

Lines changed: 1310 additions & 286 deletions

.changeset/soft-paws-marry.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@mastra/core': patch
3+
'mastracode': patch
4+
---
5+
6+
Model pack selection is now more consistent and reliable in mastracode.
7+
8+
- `/models` is now the single command for choosing and managing model packs.
9+
- Model picker ranking now learns from your recent selections and keeps those preferences across sessions.
10+
- Pack choice now restores correctly per thread when switching between threads.
11+
- Custom packs now support full create, rename, targeted edit, and delete workflows.
12+
- The built-in **Varied** option has been retired; users who had it selected are automatically migrated to a saved custom pack named `varied`.

mastracode/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ Select a suggestion with arrow keys and press Tab to insert it.
7777
| ----------------- | -------------------------------------------- |
7878
| `/new` | Start a new conversation thread |
7979
| `/threads` | List and switch between threads |
80-
| `/models` | Configure model (global/thread/mode) |
81-
| `/models:pack` | Switch model pack |
80+
| `/models` | Switch/manage model packs (built-in/custom) |
8281
| `/mode` | Switch agent mode |
8382
| `/subagents` | Configure subagent model defaults |
8483
| `/om` | Configure Observational Memory models |

mastracode/VERIFICATION.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Model Pack System Verification Checklist
2+
3+
Use this checklist to manually validate the pack-system fixes end-to-end.
4+
5+
## 1) Thread-specific pack restore
6+
7+
- [ ] In thread A, run `/models` and select a pack (for example `Anthropic`).
8+
- [ ] In thread B, run `/models` and select a different pack (for example `OpenAI`).
9+
- [ ] Switch back to thread A.
10+
- [ ] Confirm the active pack in `/models` is the one selected for thread A (`Anthropic`).
11+
- [ ] Confirm plan/build/fast mode models match thread A’s pack assignments.
12+
13+
Expected result: each thread restores its own pack selection instead of using one global pack state.
14+
15+
## 2) Model usage ranking
16+
17+
- [ ] Open `/models` and choose a pack that uses a model you can repeatedly select.
18+
- [ ] Re-select the same model multiple times (through pack switches or model picks that call `switchModel`).
19+
- [ ] Re-open the model selector.
20+
- [ ] Confirm frequently selected models appear higher in the sorted list.
21+
22+
Expected result: model ordering reflects persisted `modelUseCounts` and updates over time.
23+
24+
## 3) Command consolidation
25+
26+
- [ ] Run `/models`.
27+
- [ ] Confirm it opens the model-pack selector flow.
28+
- [ ] Run `/models:pack`.
29+
- [ ] Confirm it is not a valid command anymore.
30+
- [ ] Open `/help` and verify only `/models` is shown for model pack switching.
31+
32+
Expected result: `/models` is the single command path for pack selection.
33+
34+
## 4) Custom pack CRUD + targeted edit UX
35+
36+
- [ ] Run `/models` and select `New Custom`.
37+
- [ ] Name it `Pack-A` and choose plan/build/fast models.
38+
- [ ] Run `/models` again, select `New Custom`, create `Pack-B` with different models.
39+
- [ ] Select `Pack-A` and confirm the **Custom pack action picker** visually matches the `Switch model pack` list style (title, list, details).
40+
- [ ] Choose **Edit** and verify menu shows options with inline values, including: `Rename -> <name>`, `plan -> <model>`, `build -> <model>`, `fast -> <model>`, and `Save`.
41+
- [ ] Edit only `fast`, return to the same edit menu, then choose **Save**.
42+
- [ ] Re-open details for `Pack-A` and confirm `plan` + `build` are unchanged.
43+
- [ ] Re-open `Pack-A`, choose **Edit → Rename**, rename to `Pack-A-Renamed`, and confirm old `Pack-A` entry is gone.
44+
- [ ] Select `Pack-B`, choose **Delete**, and confirm it no longer appears.
45+
- [ ] Inspect settings persistence and confirm `customModelPacks` reflects the same final state.
46+
47+
Expected result: custom packs support create, activate, delete, and targeted edit actions without forcing all model selections.

mastracode/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { HookManager } from './hooks/index.js';
1616
import { createMcpManager } from './mcp/index.js';
1717
import type { ProviderAccess } from './onboarding/packs.js';
1818
import { getAvailableModePacks, getAvailableOmPacks } from './onboarding/packs.js';
19-
import { loadSettings, resolveModelDefaults, resolveOmModel } from './onboarding/settings.js';
19+
import { loadSettings, resolveModelDefaults, resolveOmModel, saveSettings } from './onboarding/settings.js';
2020
import { getToolCategory } from './permissions.js';
2121
import { setAuthStorage } from './providers/claude-max.js';
2222
import { setAuthStorage as setOpenAIAuthStorage } from './providers/openai-codex.js';
@@ -266,6 +266,15 @@ export async function createMastraCode(config?: MastraCodeConfig) {
266266
return undefined;
267267
},
268268
modelUseCountProvider: () => loadSettings().modelUseCounts,
269+
modelUseCountTracker: modelId => {
270+
try {
271+
const settings = loadSettings();
272+
settings.modelUseCounts[modelId] = (settings.modelUseCounts[modelId] ?? 0) + 1;
273+
saveSettings(settings);
274+
} catch (error) {
275+
console.error('Failed to persist model usage count', error);
276+
}
277+
},
269278
threadLock: {
270279
acquire: acquireThreadLock,
271280
release: releaseThreadLock,
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { migrateLegacyVariedPack, parseThreadSettings, resolveThreadActiveModelPackId } from '../settings.js';
4+
import type { GlobalSettings, StorageSettings } from '../settings.js';
5+
6+
function createSettings(overrides?: Partial<GlobalSettings>): GlobalSettings {
7+
const storage: StorageSettings = { backend: 'libsql', libsql: {}, pg: {} };
8+
return {
9+
onboarding: {
10+
completedAt: null,
11+
skippedAt: null,
12+
version: 0,
13+
modePackId: null,
14+
omPackId: null,
15+
claudeMaxOAuthWarningAcknowledgedAt: null,
16+
},
17+
models: {
18+
activeModelPackId: 'anthropic',
19+
modeDefaults: {},
20+
activeOmPackId: null,
21+
omModelOverride: null,
22+
subagentModels: {},
23+
},
24+
preferences: { yolo: null, theme: 'auto' },
25+
storage,
26+
customModelPacks: [
27+
{
28+
name: 'My Pack',
29+
models: {
30+
plan: 'openai/gpt-5.3-codex',
31+
build: 'anthropic/claude-sonnet-4-5',
32+
fast: 'openai/gpt-5.1-codex-mini',
33+
},
34+
createdAt: '2026-01-01T00:00:00.000Z',
35+
},
36+
],
37+
modelUseCounts: {},
38+
...overrides,
39+
};
40+
}
41+
42+
const builtinPacks = [
43+
{
44+
id: 'anthropic',
45+
models: {
46+
plan: 'anthropic/claude-sonnet-4-5',
47+
build: 'anthropic/claude-sonnet-4-5',
48+
fast: 'anthropic/claude-haiku-4-5',
49+
},
50+
},
51+
{
52+
id: 'openai',
53+
models: {
54+
plan: 'openai/gpt-5.3-codex',
55+
build: 'openai/gpt-5.3-codex',
56+
fast: 'openai/gpt-5.1-codex-mini',
57+
},
58+
},
59+
];
60+
61+
describe('parseThreadSettings', () => {
62+
it('extracts active pack and mode model ids from metadata', () => {
63+
const parsed = parseThreadSettings({
64+
activeModelPackId: 'custom:My Pack',
65+
modeModelId_plan: 'openai/gpt-5.3-codex',
66+
modeModelId_build: 'anthropic/claude-sonnet-4-5',
67+
ignored: 123,
68+
});
69+
70+
expect(parsed.activeModelPackId).toBe('custom:My Pack');
71+
expect(parsed.modeModelIds).toEqual({
72+
plan: 'openai/gpt-5.3-codex',
73+
build: 'anthropic/claude-sonnet-4-5',
74+
});
75+
});
76+
77+
it('returns empty values when metadata is undefined', () => {
78+
const parsed = parseThreadSettings(undefined);
79+
80+
expect(parsed.activeModelPackId).toBeNull();
81+
expect(parsed.modeModelIds).toEqual({});
82+
});
83+
});
84+
85+
describe('resolveThreadActiveModelPackId', () => {
86+
it('prefers explicit thread metadata pack id when valid', () => {
87+
const settings = createSettings();
88+
89+
const resolved = resolveThreadActiveModelPackId(settings, builtinPacks, {
90+
activeModelPackId: 'custom:My Pack',
91+
});
92+
93+
expect(resolved).toBe('custom:My Pack');
94+
});
95+
96+
it('infers pack from thread modeModelId values when explicit pack id is missing', () => {
97+
const settings = createSettings({ models: { ...createSettings().models, activeModelPackId: 'anthropic' } });
98+
99+
const resolved = resolveThreadActiveModelPackId(settings, builtinPacks, {
100+
modeModelId_plan: 'openai/gpt-5.3-codex',
101+
modeModelId_build: 'openai/gpt-5.3-codex',
102+
modeModelId_fast: 'openai/gpt-5.1-codex-mini',
103+
});
104+
105+
expect(resolved).toBe('openai');
106+
});
107+
108+
it('falls back to global activeModelPackId when no thread metadata matches', () => {
109+
const settings = createSettings({ models: { ...createSettings().models, activeModelPackId: 'anthropic' } });
110+
111+
const resolved = resolveThreadActiveModelPackId(settings, builtinPacks, {
112+
modeModelId_plan: 'unknown/model',
113+
});
114+
115+
expect(resolved).toBe('anthropic');
116+
});
117+
118+
it('returns null when global activeModelPackId points to a deleted custom pack', () => {
119+
const settings = createSettings({
120+
customModelPacks: [],
121+
models: { ...createSettings().models, activeModelPackId: 'custom:Deleted Pack' },
122+
});
123+
124+
const resolved = resolveThreadActiveModelPackId(settings, builtinPacks, {
125+
modeModelId_plan: 'unknown/model',
126+
});
127+
128+
expect(resolved).toBeNull();
129+
});
130+
});
131+
132+
describe('migrateLegacyVariedPack', () => {
133+
it('migrates legacy varied active selection to a custom varied pack', () => {
134+
const settings = createSettings({
135+
models: { ...createSettings().models, activeModelPackId: 'varied', modeDefaults: {} },
136+
onboarding: { ...createSettings().onboarding, modePackId: 'varied' },
137+
customModelPacks: [],
138+
});
139+
140+
const migrated = migrateLegacyVariedPack(settings);
141+
142+
expect(migrated).toBe(true);
143+
expect(settings.models.activeModelPackId).toBe('custom:varied');
144+
expect(settings.onboarding.modePackId).toBe('custom:varied');
145+
expect(settings.customModelPacks.find(p => p.name === 'varied')).toBeDefined();
146+
expect(settings.models.modeDefaults).toEqual({
147+
plan: 'openai/gpt-5.3-codex',
148+
build: 'anthropic/claude-sonnet-4-5',
149+
fast: 'anthropic/claude-haiku-4-5',
150+
});
151+
});
152+
});

mastracode/src/onboarding/onboarding-inline.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Box, Container, SelectList, Spacer, Text } from '@mariozechner/pi-tui';
1616
import type { Focusable, SelectItem, TUI } from '@mariozechner/pi-tui';
1717
import chalk from 'chalk';
1818
import { ANTHROPIC_OAUTH_PROVIDER_ID, CLAUDE_MAX_OAUTH_WARNING_MESSAGE } from '../auth/claude-max-warning.js';
19+
import { AskQuestionInlineComponent } from '../tui/components/ask-question-inline.js';
1920
import { theme, getSelectListTheme, mastra } from '../tui/theme.js';
2021
import type { ModePack, OMPack } from './packs.js';
2122

@@ -79,6 +80,7 @@ export class OnboardingInlineComponent extends Container implements Focusable {
7980
private currentStep: StepId = 'welcome';
8081
private stepBox!: Box;
8182
private selectList?: SelectList;
83+
private activeInlineQuestion?: AskQuestionInlineComponent;
8284
private _finished = false;
8385

8486
// Collected choices
@@ -431,10 +433,47 @@ export class OnboardingInlineComponent extends Container implements Focusable {
431433
// Custom pack flow — sequential model selection for each mode
432434
// ---------------------------------------------------------------------------
433435

436+
private async promptCustomPackName(): Promise<string | null> {
437+
return new Promise(resolve => {
438+
const question = new AskQuestionInlineComponent(
439+
{
440+
question: 'Name this custom pack',
441+
formatResult: answer => `Custom pack: ${answer}`,
442+
onSubmit: answer => {
443+
this.activeInlineQuestion = undefined;
444+
const trimmed = answer.trim();
445+
resolve(trimmed.length > 0 ? trimmed : null);
446+
},
447+
onCancel: () => {
448+
this.activeInlineQuestion = undefined;
449+
resolve(null);
450+
},
451+
},
452+
this.tui,
453+
);
454+
455+
this.activeInlineQuestion = question;
456+
this.stepBox.addChild(new Spacer(1));
457+
this.stepBox.addChild(question);
458+
this.tui.requestRender();
459+
});
460+
}
461+
434462
private async runCustomPackFlow(): Promise<void> {
435463
// Clear the pack selector so it doesn't capture input while overlays are shown
436464
this.selectList = undefined;
437-
this.collapseStep('Model pack → Custom');
465+
466+
const packName = await this.promptCustomPackName();
467+
if (!packName) {
468+
const fallback = this.options.modePacks.find(p => p.id !== 'custom') ?? this.options.modePacks[0]!;
469+
this.selectedModePack = fallback;
470+
this.collapseStep(`Model pack → ${theme.bold(this.selectedModePack.name)} (cancelled custom)`);
471+
this.renderStep('omPack');
472+
this.tui.requestRender();
473+
return;
474+
}
475+
476+
this.collapseStep(`Model pack → Custom (${packName})`);
438477

439478
const modes: Array<{ id: 'plan' | 'build' | 'fast'; label: string; color: string }> = [
440479
{ id: 'plan', label: 'plan', color: mastra.blue },
@@ -462,14 +501,14 @@ export class OnboardingInlineComponent extends Container implements Focusable {
462501
}
463502

464503
this.selectedModePack = {
465-
id: 'custom',
466-
name: 'Custom',
467-
description: 'User-selected models',
504+
id: `custom:${packName}`,
505+
name: packName,
506+
description: 'Saved custom pack',
468507
models: { build: models.build!, plan: models.plan!, fast: models.fast! },
469508
};
470509

471510
this.collapseStep(
472-
`Model pack → ${theme.bold('Custom')} ` +
511+
`Model pack → ${theme.bold(packName)} ` +
473512
`${chalk.hex(mastra.blue)('plan')} ${models.plan} ` +
474513
`${chalk.hex(mastra.purple)('build')} ${models.build} ` +
475514
`${chalk.hex(mastra.green)('fast')} ${models.fast}`,
@@ -642,6 +681,7 @@ export class OnboardingInlineComponent extends Container implements Focusable {
642681
this.stepBox.setBgFn((text: string) => theme.bg('toolSuccessBg', text));
643682
this.stepBox.addChild(new Text(`${theme.fg('success', '✓')} ${theme.fg('text', summary)}`, 0, 0));
644683
this.selectList = undefined;
684+
this.activeInlineQuestion = undefined;
645685
}
646686

647687
// ---------------------------------------------------------------------------
@@ -651,6 +691,11 @@ export class OnboardingInlineComponent extends Container implements Focusable {
651691
handleInput(data: string): void {
652692
if (this._finished) return;
653693

694+
if (this.activeInlineQuestion) {
695+
this.activeInlineQuestion.handleInput(data);
696+
return;
697+
}
698+
654699
if (this.selectList) {
655700
this.selectList.handleInput(data);
656701
return;

mastracode/src/onboarding/packs.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,6 @@ export function getAvailableModePacks(
5959
const openaiCodex = 'openai/gpt-5.3-codex';
6060
const anthropicBuild = access.anthropic === 'oauth' ? 'anthropic/claude-opus-4-6' : 'anthropic/claude-sonnet-4-5';
6161

62-
// Varied — needs both Anthropic + OpenAI. Cerebras is nice-to-have; fall
63-
// back to Haiku for the fast slot when it's missing.
64-
if (access.anthropic && access.openai) {
65-
packs.push({
66-
id: 'varied',
67-
name: 'Varied',
68-
description: 'Models from multiple providers',
69-
models: {
70-
build: anthropicBuild,
71-
plan: openaiCodex,
72-
fast: access.cerebras ? 'cerebras/zai-glm-4.7' : 'anthropic/claude-haiku-4-5',
73-
},
74-
});
75-
}
76-
7762
if (access.anthropic) {
7863
packs.push({
7964
id: 'anthropic',

0 commit comments

Comments
 (0)