Skip to content
Open
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/effort-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add a `/effort` slash command (alias `/thinking`) to adjust the model's thinking effort at runtime. `/effort <level>` sets one of `off`, `low`, `medium`, `high`, `xhigh`, `max` directly; `/effort` with no argument opens a picker highlighting the current level. Invalid levels are rejected with the list of valid values.
60 changes: 60 additions & 0 deletions apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { PermissionMode, Session } from '@moonshot-ai/kimi-code-sdk';

import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
import {
EffortSelectorComponent,
isThinkingEffortLevel,
THINKING_EFFORT_LEVELS,
type ThinkingEffortLevel,
} from '../components/dialogs/effort-selector';
import { ModelSelectorComponent } from '../components/dialogs/model-selector';
import { PermissionSelectorComponent } from '../components/dialogs/permission-selector';
import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector';
Expand Down Expand Up @@ -195,10 +201,64 @@ export function handleModelCommand(host: SlashCommandHost, args: string): void {
showModelPicker(host, alias);
}

export async function handleEffortCommand(host: SlashCommandHost, args: string): Promise<void> {
if (host.session === undefined) {
host.showError(NO_ACTIVE_SESSION_MESSAGE);
return;
}

const requested = args.trim().toLowerCase();
if (requested.length === 0) {
showEffortPicker(host);
return;
}
if (!isThinkingEffortLevel(requested)) {
host.showError(
`Unknown thinking effort: ${requested}. Valid levels: ${THINKING_EFFORT_LEVELS.join(', ')}.`,
);
return;
}
await applyEffortChoice(host, requested);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject effort levels for models without thinking support

For aliases whose capabilities do not include thinking/always_thinking and do not set adaptiveThinking, the existing /model selector forces thinking off, but this command accepts any non-off effort for the same model. That lets users enable reasoning parameters on a model the app already knows is unsupported, so the next prompt can fail at the provider instead of being rejected in the TUI. Please reuse the model capability check before applying non-off effort levels.

Useful? React with 👍 / 👎.

}

// ---------------------------------------------------------------------------
// Pickers & config apply
// ---------------------------------------------------------------------------

function showEffortPicker(host: SlashCommandHost): void {
host.mountEditorReplacement(
new EffortSelectorComponent({
currentValue: host.state.appState.thinkingLevel,
colors: host.state.theme.colors,
onSelect: (level) => {
host.restoreEditor();
void applyEffortChoice(host, level);
},
onCancel: () => {
host.restoreEditor();
},
}),
);
}

async function applyEffortChoice(host: SlashCommandHost, level: ThinkingEffortLevel): Promise<void> {
if (level === host.state.appState.thinkingLevel) {
host.showStatus(`Thinking effort unchanged: ${level}.`);
return;
}
Comment on lines +245 to +248
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-sync stale effort before treating it as unchanged

If the user sets /effort low and later uses the existing /model picker to turn thinking off, performModelSwitch only updates appState.thinking and calls session.setThinking('off'); it does not update the new thinkingLevel. After that, running /effort low hits this equality check and returns without calling setThinking, so the command says the effort is unchanged while the session remains off. Please either keep thinkingLevel in sync for the model picker path or include the boolean thinking state in this no-op check.

Useful? React with 👍 / 👎.


try {
await host.requireSession().setThinking(level);
} catch (error) {
host.showError(`Failed to set thinking effort: ${formatErrorMessage(error)}`);
return;
}

host.setAppState({ thinkingLevel: level, thinking: level !== 'off' });
host.track('thinking_toggle', { enabled: level !== 'off' });
host.showStatus(`Thinking effort set to ${level}.`, host.state.theme.colors.success);
}

function showEditorPicker(host: SlashCommandHost): void {
const currentValue = host.state.appState.editorCommand ?? '';
host.mountEditorReplacement(
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
handleAutoCommand,
handleCompactCommand,
handleEditorCommand,
handleEffortCommand,
handleModelCommand,
handlePlanCommand,
handleThemeCommand,
Expand Down Expand Up @@ -56,6 +57,7 @@ export {
handleAutoCommand,
handleCompactCommand,
handleEditorCommand,
handleEffortCommand,
handleModelCommand,
handlePlanCommand,
handleThemeCommand,
Expand Down Expand Up @@ -228,6 +230,9 @@ async function handleBuiltInSlashCommand(
case 'model':
handleModelCommand(host, args);
return;
case 'effort':
await handleEffortCommand(host, args);
return;
case 'permission':
showPermissionPicker(host);
return;
Expand Down
7 changes: 7 additions & 0 deletions apps/kimi-code/src/tui/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export const BUILTIN_SLASH_COMMANDS = [
description: 'Switch LLM model',
priority: 100,
},
{
name: 'effort',
aliases: ['thinking'],
description: 'Set model thinking effort (off/low/medium/high/xhigh/max)',
priority: 100,
availability: 'always',
},
{
name: 'help',
aliases: ['h', '?'],
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/components/chrome/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface ToolbarTip {
const TOOLBAR_TIPS: readonly ToolbarTip[] = [
{ text: 'shift+tab: plan mode' },
{ text: '/model: switch model' },
{ text: '/effort: set thinking effort' },
{ text: 'ctrl+s: steer mid-turn', priority: 2 },
{ text: '/compact: compact context', priority: 2 },
{ text: 'ctrl+o: expand tool output' },
Expand Down
58 changes: 58 additions & 0 deletions apps/kimi-code/src/tui/components/dialogs/effort-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ChoicePickerComponent, type ChoiceOption } from './choice-picker';

import type { ColorPalette } from '#/tui/theme/colors';

/** Thinking effort levels exposed to the user, in ascending order. */
export const THINKING_EFFORT_LEVELS = [
'off',
'low',
'medium',
'high',
'xhigh',
'max',
] as const;

export type ThinkingEffortLevel = (typeof THINKING_EFFORT_LEVELS)[number];

export function isThinkingEffortLevel(value: string): value is ThinkingEffortLevel {
return (THINKING_EFFORT_LEVELS as readonly string[]).includes(value);
}

const EFFORT_OPTIONS: readonly ChoiceOption[] = [
{ value: 'off', label: 'Off', description: 'Disable extended thinking — respond directly.' },
{ value: 'low', label: 'Low', description: 'Brief reasoning before responding.' },
{ value: 'medium', label: 'Medium', description: 'Moderate reasoning for everyday tasks.' },
{ value: 'high', label: 'High', description: 'Thorough reasoning. Recommended default.' },
{
value: 'xhigh',
label: 'Extra high',
description: 'Extended reasoning. Provider/model-specific; clamps to high when unsupported.',
},
{
value: 'max',
label: 'Max',
description: 'Maximum reasoning. Provider/model-specific; clamps to high when unsupported.',
},
];

export interface EffortSelectorOptions {
readonly currentValue: string;
readonly colors: ColorPalette;
readonly onSelect: (level: ThinkingEffortLevel) => void;
readonly onCancel: () => void;
}

export class EffortSelectorComponent extends ChoicePickerComponent {
constructor(opts: EffortSelectorOptions) {
super({
title: 'Set thinking effort',
options: [...EFFORT_OPTIONS],
currentValue: opts.currentValue,
colors: opts.colors,
onSelect: (value) => {
if (isThinkingEffortLevel(value)) opts.onSelect(value);
},
onCancel: opts.onCancel,
});
}
}
4 changes: 3 additions & 1 deletion apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ function createInitialAppState(input: KimiTUIStartupInput): AppState {
permissionMode: startupPermission,
planMode: input.cliOptions.plan,
thinking: false,
thinkingLevel: 'off',
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
Expand Down Expand Up @@ -951,7 +952,7 @@ export class KimiTUI {
workDir: this.state.appState.workDir,
model,
thinking:
this.session === undefined ? undefined : this.state.appState.thinking ? 'on' : 'off',
this.session === undefined ? undefined : this.state.appState.thinkingLevel || 'off',
permission: this.state.appState.permissionMode,
planMode: this.state.appState.planMode ? true : undefined,
});
Expand All @@ -971,6 +972,7 @@ export class KimiTUI {
sessionId: session.id,
model: status.model ?? '',
thinking: status.thinkingLevel !== 'off',
thinkingLevel: status.thinkingLevel,
permissionMode: status.permission,
planMode: status.planMode,
contextTokens: status.contextTokens,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface AppState {
permissionMode: PermissionMode;
planMode: boolean;
thinking: boolean;
thinkingLevel: string;
contextUsage: number;
contextTokens: number;
maxContextTokens: number;
Expand Down
9 changes: 9 additions & 0 deletions apps/kimi-code/test/tui/commands/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ describe('built-in slash command registry', () => {
expect(findBuiltInSlashCommand('unknown')).toBeUndefined();
});

it('exposes /effort with a thinking alias, always available', () => {
const effort = findBuiltInSlashCommand('effort');
expect(effort?.name).toBe('effort');
expect(effort?.aliases).toContain('thinking');
expect(findBuiltInSlashCommand('thinking')?.name).toBe('effort');
expect(resolveSlashCommandAvailability(effort!, '')).toBe('always');
});

it('marks plan clear as idle-only while normal plan toggles are always available', () => {
const plan = findBuiltInSlashCommand('plan');
expect(plan).toBeDefined();
Expand Down Expand Up @@ -80,6 +88,7 @@ describe('built-in slash command registry', () => {
expect.arrayContaining([
'compact',
'editor',
'effort',
'exit',
'export-debug-zip',
'fork',
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/create-tui-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function fakeInitialAppState(): AppState {
permissionMode: 'manual',
planMode: false,
thinking: false,
thinkingLevel: 'off',
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
Expand Down
1 change: 1 addition & 0 deletions docs/en/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Some commands are only available in the idle state. Running them while the sessi
| `/logout` | — | Clear the credentials of the currently selected account (Kimi Code OAuth credentials, or the corresponding open platform provider config). | No |
| `/connect [--refresh] [--url=<catalog-url>]` | — | Configure a provider and model from a model catalog. The default catalog is bundled with the CLI; pass `--refresh` to fetch the latest catalog from models.dev, or `--url` to read it from a custom URL. See [Providers and models — `/connect` and the model catalog](../configuration/providers.md#connect-and-the-model-catalog). | No |
| `/model` | — | Switch the LLM model used by the current session. | No |
| `/effort [<level>]` | `/thinking` | Set the model thinking effort. Pass one of `off`, `low`, `medium`, `high`, `xhigh`, `max`; with no argument, open a picker highlighting the current level. | Yes |
| `/settings` | `/config` | Open the settings panel inside the TUI. | Yes |
| `/permission` | — | Choose a permission mode. | Yes |
| `/editor` | — | Configure the external editor launched by `Ctrl-G`. | Yes |
Expand Down
1 change: 1 addition & 0 deletions docs/zh/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| `/logout` | — | 清除当前所选账号的凭据(Kimi Code OAuth 凭据,或对应开放平台的供应商配置)。 | 否 |
| `/connect [--refresh] [--url=<catalog-url>]` | — | 从模型目录中选择并配置供应商与模型。CLI 已内置默认目录;传入 `--refresh` 可从 models.dev 拉取最新目录,传入 `--url` 可指向自定义目录地址。详见 [平台与模型 — `/connect` 与模型目录](../configuration/providers.md#connect-与模型目录)。 | 否 |
| `/model` | — | 切换当前会话使用的 LLM 模型。 | 否 |
| `/effort [<level>]` | `/thinking` | 设置模型的思考强度(thinking effort)。可传入 `off`、`low`、`medium`、`high`、`xhigh`、`max` 之一;不带参数时弹出选择器并高亮当前等级。 | 是 |
| `/settings` | `/config` | 打开 TUI 内的设置面板。 | 是 |
| `/permission` | — | 选择权限模式(permission mode)。 | 是 |
| `/editor` | — | 配置 `Ctrl-G` 调起的外部编辑器。 | 是 |
Expand Down