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 cf98059b8d.

* test: remove SDK default thinking case

* fix: respect thinking mode off defaults

* fix: normalize runtime thinking requests
This commit is contained in:
liruifengv 2026-05-25 19:32:08 +08:00 committed by GitHub
parent bfbd522a71
commit 7858821f2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 133 additions and 24 deletions

View file

@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---
Persist model selections from the terminal UI to the default configuration, and honor the configured default thinking state for new sessions.

View file

@ -4349,20 +4349,16 @@ export class KimiTUI {
return;
}
if (alias === this.state.appState.model && thinking === this.state.appState.thinking) {
this.showStatus(`Already using ${alias} with thinking ${thinking ? 'on' : 'off'}.`);
return;
}
const level = thinking ? 'on' : 'off';
const prevModel = this.state.appState.model;
const prevThinking = this.state.appState.thinking;
const runtimeChanged = alias !== prevModel || thinking !== prevThinking;
const session = this.session;
try {
const session = this.session;
if (session === undefined) {
if (session === undefined && runtimeChanged) {
await this.activateModelAfterLogin(alias, thinking);
} else {
} else if (session !== undefined) {
if (alias !== prevModel) {
await session.setModel(alias);
}
@ -4370,23 +4366,50 @@ export class KimiTUI {
await session.setThinking(level);
}
}
this.setAppState({ model: alias, thinking });
if (session === undefined) {
if (alias !== prevModel) {
this.track('model_switch', { model: alias });
}
if (thinking !== prevThinking) {
this.track('thinking_toggle', { enabled: thinking });
}
}
this.showStatus(
`Switched to ${alias} with thinking ${level}.`,
this.state.theme.colors.success,
);
} catch (error) {
const msg = formatErrorMessage(error);
this.showError(`Failed to switch model: ${msg}`);
return;
}
this.setAppState({ model: alias, thinking });
if (session === undefined && runtimeChanged) {
if (alias !== prevModel) {
this.track('model_switch', { model: alias });
}
if (thinking !== prevThinking) {
this.track('thinking_toggle', { enabled: thinking });
}
}
let persisted = false;
try {
persisted = await this.persistModelSelection(alias, thinking);
} catch (error) {
const msg = formatErrorMessage(error);
this.showError(`Switched to ${alias}, but failed to save default: ${msg}`);
return;
}
const status = runtimeChanged
? `Switched to ${alias} with thinking ${level}.`
: persisted
? `Saved ${alias} with thinking ${level} as default.`
: `Already using ${alias} with thinking ${level}.`;
this.showStatus(status, this.state.theme.colors.success);
}
// Persists the selected model and thinking state as the startup defaults.
private async persistModelSelection(alias: string, thinking: boolean): Promise<boolean> {
const config = await this.harness.getConfig({ reload: true });
if (config.defaultModel === alias && config.defaultThinking === thinking) {
return false;
}
await this.harness.setConfig({
defaultModel: alias,
defaultThinking: thinking,
});
return true;
}
// Shows the theme selector.

View file

@ -117,6 +117,7 @@ function makeHarness(session = makeSession(), overrides: Record<string, unknown>
k2: { model: 'moonshot-v1', maxContextSize: 100 },
},
})),
setConfig: vi.fn(async () => ({ providers: {} })),
createSession: vi.fn(async () => session),
resumeSession: vi.fn(async () => session),
forkSession: vi.fn(async () => session),
@ -1026,6 +1027,7 @@ describe('KimiTUI message flow', () => {
it('applies /model selection with inline thinking state', async () => {
const session = makeSession();
const setConfig = vi.fn(async () => ({ providers: {} }));
const { driver } = await makeDriver(session, {
getConfig: vi.fn(async () => ({
models: {
@ -1044,7 +1046,10 @@ describe('KimiTUI message flow', () => {
capabilities: ['thinking'],
},
},
defaultModel: 'k2',
defaultThinking: false,
})),
setConfig,
});
driver.handleUserInput('/model turbo');
@ -1060,11 +1065,51 @@ describe('KimiTUI message flow', () => {
await vi.waitFor(() => {
expect(session.setModel).toHaveBeenCalledWith('turbo');
expect(session.setThinking).toHaveBeenCalledWith('on');
expect(setConfig).toHaveBeenCalledWith({
defaultModel: 'turbo',
defaultThinking: true,
});
});
expect(driver.state.appState.model).toBe('turbo');
expect(driver.state.appState.thinking).toBe(true);
});
it('persists /model selection even when runtime state is unchanged', async () => {
const session = makeSession();
const setConfig = vi.fn(async () => ({ providers: {} }));
const { driver } = await makeDriver(session, {
getConfig: vi.fn(async () => ({
models: {
k2: {
provider: 'managed:kimi-code',
model: 'kimi-k2',
maxContextSize: 100,
displayName: 'Kimi K2',
capabilities: ['thinking'],
},
},
defaultModel: 'old-default',
defaultThinking: true,
})),
setConfig,
});
driver.handleUserInput('/model k2');
const picker = driver.state.editorContainer.children[0];
expect(picker).toBeInstanceOf(ModelSelectorComponent);
(picker as ModelSelectorComponent).handleInput('\r');
await vi.waitFor(() => {
expect(setConfig).toHaveBeenCalledWith({
defaultModel: 'k2',
defaultThinking: false,
});
});
expect(session.setModel).not.toHaveBeenCalled();
expect(session.setThinking).not.toHaveBeenCalled();
});
it('deletes Kitty inline images when /new clears the transcript', async () => {
setCapabilities({ images: 'kitty', trueColor: true, hyperlinks: true });
const { driver, harness } = await makeDriver(makeSession({ id: 'ses-1' }));

View file

@ -113,7 +113,10 @@ export class ProviderManager {
}
resolveThinkingLevel(requestedThinking?: string): ThinkingEffort {
return resolveThinkingEffort(requestedThinking, this.state.config.thinking);
return resolveThinkingEffort(
resolveRuntimeThinkingRequest(requestedThinking, this.state.config.defaultThinking),
this.state.config.thinking,
);
}
resolveSelectedModel(requestedModel: string | undefined): string | undefined {
@ -137,3 +140,12 @@ function normalizeString(value: string | undefined): string | undefined {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function resolveRuntimeThinkingRequest(
requestedThinking: string | undefined,
defaultThinking: boolean | undefined,
): string | undefined {
const normalized = normalizeString(requestedThinking);
if (normalized !== undefined) return normalized;
return defaultThinking === false ? 'off' : undefined;
}

View file

@ -585,16 +585,29 @@ describe('ProviderManager thinking level', () => {
const manager = new ProviderManager({
config: {
providers: {},
defaultThinking: false,
thinking: { effort: 'medium', mode: 'auto' },
},
});
expect(manager.resolveThinkingLevel('on')).toBe('medium');
expect(manager.resolveThinkingLevel('off')).toBe('off');
expect(manager.resolveThinkingLevel('low')).toBe('low');
expect(manager.resolveThinkingLevel()).toBe('medium');
expect(manager.resolveThinkingLevel()).toBe('off');
expect(manager.resolveThinkingLevel('')).toBe('off');
expect(manager.resolveThinkingLevel(' ')).toBe('off');
const managerOnByDefault = new ProviderManager({
config: {
providers: {},
defaultThinking: true,
thinking: { effort: 'medium', mode: 'auto' },
},
});
expect(managerOnByDefault.resolveThinkingLevel()).toBe('medium');
expect(managerOnByDefault.resolveThinkingLevel(' ')).toBe('medium');
const managerWithoutEffort = new ProviderManager({
config: { providers: {}, thinking: { mode: 'auto' } },
config: { providers: {}, defaultThinking: true, thinking: { mode: 'auto' } },
});
expect(managerWithoutEffort.resolveThinkingLevel('on')).toBe('high');
expect(managerWithoutEffort.resolveThinkingLevel()).toBe('high');
@ -604,6 +617,16 @@ describe('ProviderManager thinking level', () => {
});
expect(managerOffByDefault.resolveThinkingLevel()).toBe('off');
const managerWithModeOffAndDefaultThinking = new ProviderManager({
config: {
providers: {},
defaultThinking: true,
thinking: { effort: 'medium', mode: 'off' },
},
});
expect(managerWithModeOffAndDefaultThinking.resolveThinkingLevel()).toBe('off');
expect(managerWithModeOffAndDefaultThinking.resolveThinkingLevel(' ')).toBe('off');
const managerWithoutThinking = new ProviderManager({ config: { providers: {} } });
expect(managerWithoutThinking.resolveThinkingLevel()).toBe('high');
});