Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/session-only-model-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add an Alt+S shortcut in the model picker to switch the model for the current session only, without saving it as the default.
42 changes: 29 additions & 13 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,11 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string =
currentThinking: host.state.appState.thinking,
onSelect: ({ alias, thinking }) => {
host.restoreEditor();
void performModelSwitch(host, alias, thinking);
void performModelSwitch(host, alias, thinking, true);
},
onSessionOnlySelect: ({ alias, thinking }) => {
host.restoreEditor();
void performModelSwitch(host, alias, thinking, false);
Comment thread
liruifengv marked this conversation as resolved.
},
onCancel: () => {
host.restoreEditor();
Expand All @@ -320,7 +324,12 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string =
);
}

async function performModelSwitch(host: SlashCommandHost, alias: string, thinking: boolean): Promise<void> {
async function performModelSwitch(
host: SlashCommandHost,
alias: string,
thinking: boolean,
persist: boolean,
): Promise<void> {
if (host.state.appState.streamingPhase !== 'idle') {
host.showError('Cannot switch models while streaming — press Esc or Ctrl-C first.');
return;
Expand Down Expand Up @@ -360,19 +369,26 @@ async function performModelSwitch(host: SlashCommandHost, alias: string, thinkin
}

let persisted = false;
try {
persisted = await persistModelSelection(host, alias, thinking);
} catch (error) {
const msg = formatErrorMessage(error);
host.showError(`Switched to ${alias}, but failed to save default: ${msg}`);
return;
if (persist) {
try {
persisted = await persistModelSelection(host, alias, thinking);
} catch (error) {
const msg = formatErrorMessage(error);
host.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}.`;
let status: string;
if (runtimeChanged) {
status = persist
? `Switched to ${alias} with thinking ${level}.`
: `Switched to ${alias} with thinking ${level} for this session only.`;
} else if (persist && persisted) {
status = `Saved ${alias} with thinking ${level} as default.`;
} else {
status = `Already using ${alias} with thinking ${level}.`;
}
host.showStatus(status, 'success');
}

Expand Down
17 changes: 16 additions & 1 deletion apps/kimi-code/src/tui/components/dialogs/model-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export interface ModelSelectorOptions {
* TabbedModelSelectorComponent so the inner list advertises the tab keys. */
readonly providerSwitchHint?: boolean;
readonly onSelect: (selection: ModelSelection) => void;
/** When provided, Alt+S invokes this instead of onSelect — used to apply the
* choice to the current session only, without persisting it as the default. */
readonly onSessionOnlySelect?: (selection: ModelSelection) => void;
readonly onCancel: () => void;
}

Expand Down Expand Up @@ -160,6 +163,16 @@ export class ModelSelectorComponent extends Container implements Focusable {
alias: selected.alias,
thinking: effectiveThinking(selected.model, this.draftFor(selected)),
});
return;
}

if (matchesKey(data, Key.alt('s')) && this.opts.onSessionOnlySelect !== undefined) {
const selected = this.selectedChoice();
if (selected === undefined) return;
this.opts.onSessionOnlySelect({
alias: selected.alias,
thinking: effectiveThinking(selected.model, this.draftFor(selected)),
});
}
}

Expand All @@ -179,7 +192,9 @@ export class ModelSelectorComponent extends Container implements Focusable {
if (this.opts.providerSwitchHint) hintParts.push('Tab toggle provider');
hintParts.push('↑↓ navigate');
if (searchable && view.query.length > 0) hintParts.push('Backspace clear');
hintParts.push('Enter select', 'Esc cancel');
hintParts.push('Enter select');
if (this.opts.onSessionOnlySelect !== undefined) hintParts.push('Alt+S session-only');
hintParts.push('Esc cancel');

const lines: string[] = [
currentTheme.fg('primary', '─'.repeat(width)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export interface TabbedModelSelectorOptions {
* tab derived from `currentValue`. */
readonly initialTabId?: string;
readonly onSelect: (selection: ModelSelection) => void;
/** Forwarded to each inner selector; when set, Alt+S applies the choice to
* the current session only without persisting it as the default. */
readonly onSessionOnlySelect?: (selection: ModelSelection) => void;
readonly onCancel: () => void;
}

Expand Down Expand Up @@ -250,6 +253,7 @@ function makeSelector(
searchable: true,
providerSwitchHint: true,
onSelect: opts.onSelect,
onSessionOnlySelect: opts.onSessionOnlySelect,
onCancel: opts.onCancel,
};
return new ModelSelectorComponent(inner);
Expand Down
46 changes: 46 additions & 0 deletions apps/kimi-code/test/tui/components/dialogs/model-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,50 @@ describe('ModelSelectorComponent', () => {
}
}
});

it('invokes onSessionOnlySelect on Alt+S with the effective thinking state', () => {
const onSelect = vi.fn();
const onSessionOnlySelect = vi.fn();
const picker = new ModelSelectorComponent({
models: { kimi: model('Kimi K2', ['thinking']) },
currentValue: 'kimi',
currentThinking: true,
onSelect,
onSessionOnlySelect,
onCancel: vi.fn(),
});

// Toggle thinking Off, then Alt+S applies the choice to the session only.
picker.handleInput(RIGHT);
picker.handleInput(`${ESC}s`);
expect(onSessionOnlySelect).toHaveBeenCalledWith({ alias: 'kimi', thinking: false });
expect(onSelect).not.toHaveBeenCalled();
});

it('ignores Alt+S and hides its hint when onSessionOnlySelect is not provided', () => {
const onSelect = vi.fn();
const picker = new ModelSelectorComponent({
models: { kimi: model('Kimi K2') },
currentValue: 'kimi',
currentThinking: true,
onSelect,
onCancel: vi.fn(),
});

picker.handleInput(`${ESC}s`);
expect(onSelect).not.toHaveBeenCalled();
expect(text(picker)).not.toContain('Alt+S session-only');
});

it('shows the Alt+S session-only hint when onSessionOnlySelect is provided', () => {
const picker = new ModelSelectorComponent({
models: { kimi: model('Kimi K2') },
currentValue: 'kimi',
currentThinking: true,
onSelect: vi.fn(),
onSessionOnlySelect: vi.fn(),
onCancel: vi.fn(),
});
expect(text(picker)).toContain('Alt+S session-only');
});
});
45 changes: 45 additions & 0 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3411,6 +3411,51 @@ command = "vim"
expect(driver.state.appState.thinking).toBe(true);
});

it('applies /model selection to the session only on Alt+S without persisting', 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'],
},
turbo: {
provider: 'managed:kimi-code',
model: 'kimi-turbo',
maxContextSize: 100,
displayName: 'Kimi Turbo',
capabilities: ['thinking'],
},
},
defaultModel: 'k2',
defaultThinking: false,
})),
setConfig,
});

driver.handleUserInput('/model turbo');

await vi.waitFor(() => {
expect(driver.state.editorContainer.children[0]).toBeInstanceOf(TabbedModelSelectorComponent);
});
const picker = driver.state.editorContainer.children[0];
// /model turbo preselects turbo; Alt+S applies it to the current session only.
(picker as TabbedModelSelectorComponent).handleInput(`${ESC}s`);

await vi.waitFor(() => {
expect(session.setModel).toHaveBeenCalledWith('turbo');
expect(session.setThinking).toHaveBeenCalledWith('on');
});
expect(setConfig).not.toHaveBeenCalled();
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: {} }));
Expand Down
Loading