diff --git a/.changeset/persist-runtime-model-defaults.md b/.changeset/persist-runtime-model-defaults.md new file mode 100644 index 000000000..997cbcff6 --- /dev/null +++ b/.changeset/persist-runtime-model-defaults.md @@ -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. diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 28727c01d..91ea2549b 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -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 { + 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. diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 4edeb7780..e3b683aee 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -117,6 +117,7 @@ function makeHarness(session = makeSession(), overrides: Record 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' })); diff --git a/packages/agent-core/src/providers/provider-manager.ts b/packages/agent-core/src/providers/provider-manager.ts index 7c2bc14f1..bebd3f3e7 100644 --- a/packages/agent-core/src/providers/provider-manager.ts +++ b/packages/agent-core/src/providers/provider-manager.ts @@ -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; +} diff --git a/packages/agent-core/test/harness/runtime-provider.test.ts b/packages/agent-core/test/harness/runtime-provider.test.ts index 31264e800..07367a5c8 100644 --- a/packages/agent-core/test/harness/runtime-provider.test.ts +++ b/packages/agent-core/test/harness/runtime-provider.test.ts @@ -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'); });