Skip to content

Commit 7858821

Browse files
authored
fix: persist TUI model selection defaults (#24)
* Persist runtime model and default thinking * fix: persist model defaults from TUI * refactor: centralize default thinking resolution * fix: wait for background shutdown notifications * Revert "fix: wait for background shutdown notifications" This reverts commit cf98059. * test: remove SDK default thinking case * fix: respect thinking mode off defaults * fix: normalize runtime thinking requests
1 parent bfbd522 commit 7858821

5 files changed

Lines changed: 133 additions & 24 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@moonshot-ai/agent-core": patch
3+
"@moonshot-ai/kimi-code": patch
4+
---
5+
6+
Persist model selections from the terminal UI to the default configuration, and honor the configured default thinking state for new sessions.

apps/kimi-code/src/tui/kimi-tui.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4349,44 +4349,67 @@ export class KimiTUI {
43494349
return;
43504350
}
43514351

4352-
if (alias === this.state.appState.model && thinking === this.state.appState.thinking) {
4353-
this.showStatus(`Already using ${alias} with thinking ${thinking ? 'on' : 'off'}.`);
4354-
return;
4355-
}
4356-
43574352
const level = thinking ? 'on' : 'off';
43584353
const prevModel = this.state.appState.model;
43594354
const prevThinking = this.state.appState.thinking;
4355+
const runtimeChanged = alias !== prevModel || thinking !== prevThinking;
43604356

4357+
const session = this.session;
43614358
try {
4362-
const session = this.session;
4363-
if (session === undefined) {
4359+
if (session === undefined && runtimeChanged) {
43644360
await this.activateModelAfterLogin(alias, thinking);
4365-
} else {
4361+
} else if (session !== undefined) {
43664362
if (alias !== prevModel) {
43674363
await session.setModel(alias);
43684364
}
43694365
if (thinking !== prevThinking) {
43704366
await session.setThinking(level);
43714367
}
43724368
}
4373-
this.setAppState({ model: alias, thinking });
4374-
if (session === undefined) {
4375-
if (alias !== prevModel) {
4376-
this.track('model_switch', { model: alias });
4377-
}
4378-
if (thinking !== prevThinking) {
4379-
this.track('thinking_toggle', { enabled: thinking });
4380-
}
4381-
}
4382-
this.showStatus(
4383-
`Switched to ${alias} with thinking ${level}.`,
4384-
this.state.theme.colors.success,
4385-
);
43864369
} catch (error) {
43874370
const msg = formatErrorMessage(error);
43884371
this.showError(`Failed to switch model: ${msg}`);
4372+
return;
43894373
}
4374+
4375+
this.setAppState({ model: alias, thinking });
4376+
if (session === undefined && runtimeChanged) {
4377+
if (alias !== prevModel) {
4378+
this.track('model_switch', { model: alias });
4379+
}
4380+
if (thinking !== prevThinking) {
4381+
this.track('thinking_toggle', { enabled: thinking });
4382+
}
4383+
}
4384+
4385+
let persisted = false;
4386+
try {
4387+
persisted = await this.persistModelSelection(alias, thinking);
4388+
} catch (error) {
4389+
const msg = formatErrorMessage(error);
4390+
this.showError(`Switched to ${alias}, but failed to save default: ${msg}`);
4391+
return;
4392+
}
4393+
4394+
const status = runtimeChanged
4395+
? `Switched to ${alias} with thinking ${level}.`
4396+
: persisted
4397+
? `Saved ${alias} with thinking ${level} as default.`
4398+
: `Already using ${alias} with thinking ${level}.`;
4399+
this.showStatus(status, this.state.theme.colors.success);
4400+
}
4401+
4402+
// Persists the selected model and thinking state as the startup defaults.
4403+
private async persistModelSelection(alias: string, thinking: boolean): Promise<boolean> {
4404+
const config = await this.harness.getConfig({ reload: true });
4405+
if (config.defaultModel === alias && config.defaultThinking === thinking) {
4406+
return false;
4407+
}
4408+
await this.harness.setConfig({
4409+
defaultModel: alias,
4410+
defaultThinking: thinking,
4411+
});
4412+
return true;
43904413
}
43914414

43924415
// Shows the theme selector.

apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function makeHarness(session = makeSession(), overrides: Record<string, unknown>
117117
k2: { model: 'moonshot-v1', maxContextSize: 100 },
118118
},
119119
})),
120+
setConfig: vi.fn(async () => ({ providers: {} })),
120121
createSession: vi.fn(async () => session),
121122
resumeSession: vi.fn(async () => session),
122123
forkSession: vi.fn(async () => session),
@@ -1026,6 +1027,7 @@ describe('KimiTUI message flow', () => {
10261027

10271028
it('applies /model selection with inline thinking state', async () => {
10281029
const session = makeSession();
1030+
const setConfig = vi.fn(async () => ({ providers: {} }));
10291031
const { driver } = await makeDriver(session, {
10301032
getConfig: vi.fn(async () => ({
10311033
models: {
@@ -1044,7 +1046,10 @@ describe('KimiTUI message flow', () => {
10441046
capabilities: ['thinking'],
10451047
},
10461048
},
1049+
defaultModel: 'k2',
1050+
defaultThinking: false,
10471051
})),
1052+
setConfig,
10481053
});
10491054

10501055
driver.handleUserInput('/model turbo');
@@ -1060,11 +1065,51 @@ describe('KimiTUI message flow', () => {
10601065
await vi.waitFor(() => {
10611066
expect(session.setModel).toHaveBeenCalledWith('turbo');
10621067
expect(session.setThinking).toHaveBeenCalledWith('on');
1068+
expect(setConfig).toHaveBeenCalledWith({
1069+
defaultModel: 'turbo',
1070+
defaultThinking: true,
1071+
});
10631072
});
10641073
expect(driver.state.appState.model).toBe('turbo');
10651074
expect(driver.state.appState.thinking).toBe(true);
10661075
});
10671076

1077+
it('persists /model selection even when runtime state is unchanged', async () => {
1078+
const session = makeSession();
1079+
const setConfig = vi.fn(async () => ({ providers: {} }));
1080+
const { driver } = await makeDriver(session, {
1081+
getConfig: vi.fn(async () => ({
1082+
models: {
1083+
k2: {
1084+
provider: 'managed:kimi-code',
1085+
model: 'kimi-k2',
1086+
maxContextSize: 100,
1087+
displayName: 'Kimi K2',
1088+
capabilities: ['thinking'],
1089+
},
1090+
},
1091+
defaultModel: 'old-default',
1092+
defaultThinking: true,
1093+
})),
1094+
setConfig,
1095+
});
1096+
1097+
driver.handleUserInput('/model k2');
1098+
1099+
const picker = driver.state.editorContainer.children[0];
1100+
expect(picker).toBeInstanceOf(ModelSelectorComponent);
1101+
(picker as ModelSelectorComponent).handleInput('\r');
1102+
1103+
await vi.waitFor(() => {
1104+
expect(setConfig).toHaveBeenCalledWith({
1105+
defaultModel: 'k2',
1106+
defaultThinking: false,
1107+
});
1108+
});
1109+
expect(session.setModel).not.toHaveBeenCalled();
1110+
expect(session.setThinking).not.toHaveBeenCalled();
1111+
});
1112+
10681113
it('deletes Kitty inline images when /new clears the transcript', async () => {
10691114
setCapabilities({ images: 'kitty', trueColor: true, hyperlinks: true });
10701115
const { driver, harness } = await makeDriver(makeSession({ id: 'ses-1' }));

packages/agent-core/src/providers/provider-manager.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,10 @@ export class ProviderManager {
113113
}
114114

115115
resolveThinkingLevel(requestedThinking?: string): ThinkingEffort {
116-
return resolveThinkingEffort(requestedThinking, this.state.config.thinking);
116+
return resolveThinkingEffort(
117+
resolveRuntimeThinkingRequest(requestedThinking, this.state.config.defaultThinking),
118+
this.state.config.thinking,
119+
);
117120
}
118121

119122
resolveSelectedModel(requestedModel: string | undefined): string | undefined {
@@ -137,3 +140,12 @@ function normalizeString(value: string | undefined): string | undefined {
137140
const trimmed = value.trim();
138141
return trimmed.length > 0 ? trimmed : undefined;
139142
}
143+
144+
function resolveRuntimeThinkingRequest(
145+
requestedThinking: string | undefined,
146+
defaultThinking: boolean | undefined,
147+
): string | undefined {
148+
const normalized = normalizeString(requestedThinking);
149+
if (normalized !== undefined) return normalized;
150+
return defaultThinking === false ? 'off' : undefined;
151+
}

packages/agent-core/test/harness/runtime-provider.test.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,16 +585,29 @@ describe('ProviderManager thinking level', () => {
585585
const manager = new ProviderManager({
586586
config: {
587587
providers: {},
588+
defaultThinking: false,
588589
thinking: { effort: 'medium', mode: 'auto' },
589590
},
590591
});
591592
expect(manager.resolveThinkingLevel('on')).toBe('medium');
592593
expect(manager.resolveThinkingLevel('off')).toBe('off');
593594
expect(manager.resolveThinkingLevel('low')).toBe('low');
594-
expect(manager.resolveThinkingLevel()).toBe('medium');
595+
expect(manager.resolveThinkingLevel()).toBe('off');
596+
expect(manager.resolveThinkingLevel('')).toBe('off');
597+
expect(manager.resolveThinkingLevel(' ')).toBe('off');
598+
599+
const managerOnByDefault = new ProviderManager({
600+
config: {
601+
providers: {},
602+
defaultThinking: true,
603+
thinking: { effort: 'medium', mode: 'auto' },
604+
},
605+
});
606+
expect(managerOnByDefault.resolveThinkingLevel()).toBe('medium');
607+
expect(managerOnByDefault.resolveThinkingLevel(' ')).toBe('medium');
595608

596609
const managerWithoutEffort = new ProviderManager({
597-
config: { providers: {}, thinking: { mode: 'auto' } },
610+
config: { providers: {}, defaultThinking: true, thinking: { mode: 'auto' } },
598611
});
599612
expect(managerWithoutEffort.resolveThinkingLevel('on')).toBe('high');
600613
expect(managerWithoutEffort.resolveThinkingLevel()).toBe('high');
@@ -604,6 +617,16 @@ describe('ProviderManager thinking level', () => {
604617
});
605618
expect(managerOffByDefault.resolveThinkingLevel()).toBe('off');
606619

620+
const managerWithModeOffAndDefaultThinking = new ProviderManager({
621+
config: {
622+
providers: {},
623+
defaultThinking: true,
624+
thinking: { effort: 'medium', mode: 'off' },
625+
},
626+
});
627+
expect(managerWithModeOffAndDefaultThinking.resolveThinkingLevel()).toBe('off');
628+
expect(managerWithModeOffAndDefaultThinking.resolveThinkingLevel(' ')).toBe('off');
629+
607630
const managerWithoutThinking = new ProviderManager({ config: { providers: {} } });
608631
expect(managerWithoutThinking.resolveThinkingLevel()).toBe('high');
609632
});

0 commit comments

Comments
 (0)