From de4cdeeb43731eb571020050e028449a0e851424 Mon Sep 17 00:00:00 2001 From: haosenwang1018 <1293965075@qq.com> Date: Mon, 1 Jun 2026 20:24:06 +0800 Subject: [PATCH] feat(tui): persist default permission mode --- .../persist-default-permission-command.md | 5 + apps/kimi-code/src/tui/commands/config.ts | 110 +++++++++++++++++- apps/kimi-code/src/tui/commands/dispatch.ts | 4 +- apps/kimi-code/src/tui/commands/registry.ts | 2 +- .../components/dialogs/settings-selector.ts | 14 ++- .../components/dialogs/choice-picker.test.ts | 2 + .../test/tui/kimi-tui-message-flow.test.ts | 35 ++++++ docs/en/reference/slash-commands.md | 2 +- docs/zh/reference/slash-commands.md | 2 +- 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 .changeset/persist-default-permission-command.md diff --git a/.changeset/persist-default-permission-command.md b/.changeset/persist-default-permission-command.md new file mode 100644 index 00000000..7b62e800 --- /dev/null +++ b/.changeset/persist-default-permission-command.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add a persistent default permission mode picker and `/permission default ` command for new sessions. diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index c70bc8a2..f822803a 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -17,6 +17,14 @@ import type { SlashCommandHost } from './dispatch'; // Plan / Config commands // --------------------------------------------------------------------------- +const PERMISSION_COMMAND_USAGE = + 'Usage: /permission [manual|auto|yolo] or /permission default '; + +function parsePermissionMode(value: string): PermissionMode | undefined { + if (value === 'manual' || value === 'auto' || value === 'yolo') return value; + return undefined; +} + export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; if (session === undefined) { @@ -150,6 +158,35 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P } } +export async function handlePermissionCommand(host: SlashCommandHost, args: string): Promise { + const tokens = args + .trim() + .toLowerCase() + .split(/\s+/) + .filter((token) => token.length > 0); + if (tokens.length === 0) { + showPermissionPicker(host); + return; + } + + if (tokens[0] === 'default') { + const mode = tokens.length === 2 ? parsePermissionMode(tokens[1] ?? '') : undefined; + if (mode === undefined) { + host.showError(PERMISSION_COMMAND_USAGE); + return; + } + await applyDefaultPermissionChoice(host, mode); + return; + } + + const mode = tokens.length === 1 ? parsePermissionMode(tokens[0] ?? '') : undefined; + if (mode === undefined) { + host.showError(PERMISSION_COMMAND_USAGE); + return; + } + await applyPermissionChoice(host, mode); +} + export async function handleCompactCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; if (session === undefined) { @@ -403,13 +440,19 @@ export function showPermissionPicker(host: SlashCommandHost): void { } async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMode): Promise { + const session = host.session; + if (session === undefined) { + host.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + if (mode === host.state.appState.permissionMode) { host.showStatus(`Permission mode unchanged: ${mode}.`); return; } try { - await host.requireSession().setPermission(mode); + await session.setPermission(mode); } catch (error) { const msg = formatErrorMessage(error); host.showError(`Failed to set permission mode: ${msg}`); @@ -420,6 +463,70 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod host.showNotice(`Permission mode: ${mode}`); } +function showDefaultPermissionPicker(host: SlashCommandHost): void { + void (async () => { + let currentValue: PermissionMode = 'manual'; + try { + const config = await host.harness.getConfig({ reload: true }); + currentValue = config.defaultPermissionMode ?? 'manual'; + } catch { + currentValue = host.state.appState.permissionMode; + } + + host.mountEditorReplacement( + new PermissionSelectorComponent({ + currentValue, + colors: host.state.theme.colors, + onSelect: (value) => { + host.restoreEditor(); + void applyDefaultPermissionChoice(host, value); + }, + onCancel: () => { + host.restoreEditor(); + }, + }), + ); + })(); +} + +async function applyDefaultPermissionChoice(host: SlashCommandHost, mode: PermissionMode): Promise { + let persisted = false; + try { + const config = await host.harness.getConfig({ reload: true }); + if (config.defaultPermissionMode !== mode) { + await host.harness.setConfig({ defaultPermissionMode: mode }); + persisted = true; + } + } catch (error) { + const msg = formatErrorMessage(error); + host.showError(`Failed to save default permission mode: ${msg}`); + return; + } + + let currentUpdated = false; + const session = host.session; + if (session !== undefined && mode !== host.state.appState.permissionMode) { + try { + await session.setPermission(mode); + host.setAppState({ permissionMode: mode }); + currentUpdated = true; + } catch (error) { + const msg = formatErrorMessage(error); + const prefix = persisted + ? `Default permission mode set to ${mode}` + : `Default permission mode already ${mode}`; + host.showError(`${prefix}, but failed to update current session: ${msg}`); + return; + } + } + + const status = persisted + ? `Default permission mode set to ${mode}.` + : `Default permission mode already ${mode}.`; + const detail = currentUpdated ? ' Current session updated.' : ''; + host.showStatus(`${status}${detail}`, host.state.theme.colors.success); +} + export function showSettingsSelector(host: SlashCommandHost): void { host.mountEditorReplacement( new SettingsSelectorComponent({ @@ -439,6 +546,7 @@ function handleSettingsSelection(host: SlashCommandHost, value: SettingsSelectio switch (value) { case 'model': showModelPicker(host); return; case 'permission': showPermissionPicker(host); return; + case 'default-permission': showDefaultPermissionPicker(host); return; case 'theme': showThemePicker(host); return; case 'editor': showEditorPicker(host); return; case 'usage': void showUsage(host); return; diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index 2dc39ba1..00d0abf6 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -26,6 +26,7 @@ import { handleCompactCommand, handleEditorCommand, handleModelCommand, + handlePermissionCommand, handlePlanCommand, handleThemeCommand, handleYoloCommand, @@ -58,6 +59,7 @@ export { handleCompactCommand, handleEditorCommand, handleModelCommand, + handlePermissionCommand, handlePlanCommand, handleThemeCommand, handleYoloCommand, @@ -233,7 +235,7 @@ async function handleBuiltInSlashCommand( await handleProviderCommand(host); return; case 'permission': - showPermissionPicker(host); + await handlePermissionCommand(host, args); return; case 'settings': showSettingsSelector(host); diff --git a/apps/kimi-code/src/tui/commands/registry.ts b/apps/kimi-code/src/tui/commands/registry.ts index 4f4b7584..6601bad0 100644 --- a/apps/kimi-code/src/tui/commands/registry.ts +++ b/apps/kimi-code/src/tui/commands/registry.ts @@ -18,7 +18,7 @@ export const BUILTIN_SLASH_COMMANDS = [ { name: 'permission', aliases: [], - description: 'Select permission mode', + description: 'Select or persist permission mode', priority: 100, availability: 'always', }, diff --git a/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts b/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts index fb224345..ad852276 100644 --- a/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts +++ b/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts @@ -2,7 +2,13 @@ import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; import type { ColorPalette } from '#/tui/theme/colors'; -export type SettingsSelection = 'model' | 'theme' | 'editor' | 'permission' | 'usage'; +export type SettingsSelection = + | 'model' + | 'theme' + | 'editor' + | 'permission' + | 'default-permission' + | 'usage'; const SETTINGS_OPTIONS: readonly ChoiceOption[] = [ { @@ -15,6 +21,11 @@ const SETTINGS_OPTIONS: readonly ChoiceOption[] = [ label: 'Permission', description: 'Choose how tool actions are approved.', }, + { + value: 'default-permission', + label: 'Default Permission', + description: 'Persist the permission mode for new sessions.', + }, { value: 'theme', label: 'Theme', @@ -38,6 +49,7 @@ function isSettingsSelection(value: string): value is SettingsSelection { value === 'theme' || value === 'editor' || value === 'permission' || + value === 'default-permission' || value === 'usage' ); } diff --git a/apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts b/apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts index 7f47bbe1..42e8f02a 100644 --- a/apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts @@ -100,6 +100,8 @@ describe('ChoicePickerComponent', () => { const settingsOutput = settings.render(120).map(strip); expect(settingsOutput).toContain(' ❯ Model'); expect(settingsOutput).toContain(' Switch the active model and thinking mode.'); + expect(settingsOutput).toContain(' Default Permission'); + expect(settingsOutput).toContain(' Persist the permission mode for new sessions.'); }); it('submits the selected model and inline thinking state', () => { 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 7e547d50..c049fdde 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 @@ -529,6 +529,41 @@ describe('KimiTUI message flow', () => { expect(harness.track).not.toHaveBeenCalledWith('yolo_toggle', expect.anything()); }); + it('sets permission mode directly from /permission arguments', async () => { + const { driver, session, harness } = await makeDriver(); + + driver.handleUserInput('/permission auto'); + + await vi.waitFor(() => { + expect(session.setPermission).toHaveBeenCalledWith('auto'); + }); + expect(driver.state.appState.permissionMode).toBe('auto'); + expect(harness.setConfig).not.toHaveBeenCalled(); + }); + + it('persists /permission default and applies it to the active session', async () => { + const session = makeSession(); + const getConfig = vi.fn(async () => ({ + models: { + k2: { model: 'moonshot-v1', maxContextSize: 100 }, + }, + defaultPermissionMode: 'manual', + })); + const setConfig = vi.fn(async () => ({ + providers: {}, + defaultPermissionMode: 'yolo', + })); + const { driver } = await makeDriver(session, { getConfig, setConfig }); + + driver.handleUserInput('/permission default yolo'); + + await vi.waitFor(() => { + expect(setConfig).toHaveBeenCalledWith({ defaultPermissionMode: 'yolo' }); + expect(session.setPermission).toHaveBeenCalledWith('yolo'); + }); + expect(driver.state.appState.permissionMode).toBe('yolo'); + }); + it('hydrates MCP server status after subscribing to session events', async () => { const session = makeSession({ listMcpServers: vi.fn(async () => [ diff --git a/docs/en/reference/slash-commands.md b/docs/en/reference/slash-commands.md index 4f2a56a0..b8212a74 100644 --- a/docs/en/reference/slash-commands.md +++ b/docs/en/reference/slash-commands.md @@ -17,7 +17,7 @@ Some commands are only available in the idle state. Running them while the sessi | `/provider` | — | Open the interactive provider manager to view, add, and delete configured providers. See [Providers and models — `/provider` and provider management](../configuration/providers.md#provider-and-provider-management). | Yes | | `/model` | — | Switch the LLM model used by the current session. | Yes | | `/settings` | `/config` | Open the settings panel inside the TUI. | Yes | -| `/permission` | — | Choose a permission mode. | Yes | +| `/permission [manual\|auto\|yolo]` | — | Choose a permission mode for the current session; run `/permission default ` to persist the default for new sessions. | Yes | | `/editor` | — | Configure the external editor launched by `Ctrl-G`. | Yes | | `/theme` | — | Switch the terminal UI color theme. | Yes | diff --git a/docs/zh/reference/slash-commands.md b/docs/zh/reference/slash-commands.md index af504f2c..6ceb7742 100644 --- a/docs/zh/reference/slash-commands.md +++ b/docs/zh/reference/slash-commands.md @@ -17,7 +17,7 @@ | `/provider` | — | 打开交互式供应商管理器,查看、添加和删除已配置的供应商。详见 [平台与模型 — `/provider` 与供应商管理](../configuration/providers.md#provider-与供应商管理)。 | 是 | | `/model` | — | 切换当前会话使用的 LLM 模型。 | 是 | | `/settings` | `/config` | 打开 TUI 内的设置面板。 | 是 | -| `/permission` | — | 选择权限模式(permission mode)。 | 是 | +| `/permission [manual\|auto\|yolo]` | — | 选择当前会话的权限模式;运行 `/permission default ` 可持久化新会话默认模式。 | 是 | | `/editor` | — | 配置 `Ctrl-G` 调起的外部编辑器。 | 是 | | `/theme` | — | 切换终端 UI 配色主题。 | 是 |