From 9da3ae98fae8d449e14734f4349be08262940875 Mon Sep 17 00:00:00 2001 From: 7Sageer <125936732+7Sageer@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:52:07 +0800 Subject: [PATCH 1/8] feat(kimi-code): show file-change warning when switching to ask-when-needed or never-ask mode --- .changeset/permission-mode-file-warning.md | 5 ++++ apps/kimi-code/src/tui/commands/config.ts | 27 +++++++++++++++---- .../test/tui/kimi-tui-message-flow.test.ts | 6 +++++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 .changeset/permission-mode-file-warning.md diff --git a/.changeset/permission-mode-file-warning.md b/.changeset/permission-mode-file-warning.md new file mode 100644 index 00000000000..eb7baebb142 --- /dev/null +++ b/.changeset/permission-mode-file-warning.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Show a warning that Kimi Code can modify or delete files without confirmation after switching to Ask When Needed or Never Ask mode. diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index 4832e930e37..3dc99c65f2b 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -42,6 +42,8 @@ const MODEL_SWITCH_CACHE_WARNING = 'Note: Switching models invalidates the existing prompt cache. Use /new to avoid extra token costs.'; const EFFORT_SWITCH_CACHE_WARNING = 'Note: Switching effort invalidates the existing prompt cache. Use /new to avoid extra token costs.'; +const UNCONFIRMED_FILE_CHANGES_WARNING = + 'In this mode, Kimi Code can modify or delete files without your confirmation'; /** True once the conversation has at least one user message: a switch from * then on resends the accumulated context, losing the cache. Shell-command @@ -145,7 +147,10 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P } await session?.setPermission('yolo'); host.setAppState({ permissionMode: 'yolo' }); - host.showNotice('Ask When Needed mode: ON', 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.'); + host.showNotice( + 'Ask When Needed mode: ON', + `Routine edits and commands run automatically; risky actions, questions, and plans still ask.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + ); return; } @@ -168,7 +173,10 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P } else { await session?.setPermission('yolo'); host.setAppState({ permissionMode: 'yolo' }); - host.showNotice('Ask When Needed mode: ON', 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.'); + host.showNotice( + 'Ask When Needed mode: ON', + `Routine edits and commands run automatically; risky actions, questions, and plans still ask.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + ); } } @@ -191,7 +199,10 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P } await session?.setPermission('auto'); host.setAppState({ permissionMode: 'auto' }); - host.showNotice('Never Ask mode: ON', 'Never interrupts you; everything runs and is decided automatically.'); + host.showNotice( + 'Never Ask mode: ON', + `Never interrupts you; everything runs and is decided automatically.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + ); return; } @@ -214,7 +225,10 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P } else { await session?.setPermission('auto'); host.setAppState({ permissionMode: 'auto' }); - host.showNotice('Never Ask mode: ON', 'Never interrupts you; everything runs and is decided automatically.'); + host.showNotice( + 'Never Ask mode: ON', + `Never interrupts you; everything runs and is decided automatically.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + ); } } @@ -906,7 +920,10 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod } host.setAppState({ permissionMode: mode }); - host.showNotice(`Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`); + host.showNotice( + `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, + mode === 'manual' ? undefined : UNCONFIRMED_FILE_CHANGES_WARNING, + ); } export function showSettingsSelector(host: SlashCommandHost): void { 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 0be3b47c4fe..0528888f14f 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 @@ -2613,6 +2613,9 @@ command = "vim" expect(driver.state.appState).toMatchObject({ permissionMode: 'yolo', }); + expect(stripSgr(renderTranscript(driver))).toContain( + 'In this mode, Kimi Code can modify or delete files without your confirmation', + ); expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'ask-when-needed' }); expect(harness.track).not.toHaveBeenCalledWith('yolo_toggle', expect.anything()); }); @@ -2856,6 +2859,9 @@ command = "vim" expect(transcript).not.toContain('hello'); expect(transcript).not.toContain('Cannot undo 10 prompts'); expect(transcript).toContain('Never Ask mode: ON'); + expect(transcript).toContain( + 'In this mode, Kimi Code can modify or delete files without your confirmation', + ); expect(driver.state.appState.permissionMode).toBe('auto'); }); From 58d95f57cba7285ea94621f3466e0103bd0ebc52 Mon Sep 17 00:00:00 2001 From: 7Sageer Date: Wed, 2 Sep 2026 17:53:19 +0800 Subject: [PATCH 2/8] Update permission-mode-file-warning.md Signed-off-by: 7Sageer --- .changeset/permission-mode-file-warning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/permission-mode-file-warning.md b/.changeset/permission-mode-file-warning.md index eb7baebb142..7dc75fdd79f 100644 --- a/.changeset/permission-mode-file-warning.md +++ b/.changeset/permission-mode-file-warning.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": patch --- -Show a warning that Kimi Code can modify or delete files without confirmation after switching to Ask When Needed or Never Ask mode. +Show a warning after switching to Ask When Needed or Never Ask mode. From 7613f0c93fc2ec7b06af92b6bb4a641c8dd24651 Mon Sep 17 00:00:00 2001 From: 7Sageer <125936732+7Sageer@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:04:23 +0800 Subject: [PATCH 3/8] feat(kimi-code): show file-change warning on goal and swarm permission switches --- apps/kimi-code/src/tui/commands/config.ts | 4 +--- apps/kimi-code/src/tui/commands/goal.ts | 10 ++++++++++ apps/kimi-code/src/tui/commands/swarm.ts | 5 +++++ apps/kimi-code/src/tui/utils/permission-mode.ts | 3 +++ apps/kimi-code/test/tui/commands/goal.test.ts | 12 ++++++++++++ apps/kimi-code/test/tui/commands/swarm.test.ts | 11 +++++++++++ 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index 3dc99c65f2b..e3762f6bfc4 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -26,7 +26,7 @@ import type { ThemeName } from '#/tui/theme'; import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme'; import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; -import { PERMISSION_MODE_DISPLAY_NAMES } from '../utils/permission-mode'; +import { PERMISSION_MODE_DISPLAY_NAMES, UNCONFIRMED_FILE_CHANGES_WARNING } from '../utils/permission-mode'; import { thinkingEffortToConfig } from '../utils/thinking-config'; import { showUsage } from './info'; import { setExperimentalFeatures } from './experimental-flags'; @@ -42,8 +42,6 @@ const MODEL_SWITCH_CACHE_WARNING = 'Note: Switching models invalidates the existing prompt cache. Use /new to avoid extra token costs.'; const EFFORT_SWITCH_CACHE_WARNING = 'Note: Switching effort invalidates the existing prompt cache. Use /new to avoid extra token costs.'; -const UNCONFIRMED_FILE_CHANGES_WARNING = - 'In this mode, Kimi Code can modify or delete files without your confirmation'; /** True once the conversation has at least one user message: a switch from * then on resends the accumulated context, losing the cache. Shell-command diff --git a/apps/kimi-code/src/tui/commands/goal.ts b/apps/kimi-code/src/tui/commands/goal.ts index 169234b4552..f79230d82a6 100644 --- a/apps/kimi-code/src/tui/commands/goal.ts +++ b/apps/kimi-code/src/tui/commands/goal.ts @@ -25,6 +25,7 @@ import { type GoalQueueSnapshot, } from '../goal-queue-store'; import { formatErrorMessage } from '../utils/event-payload'; +import { PERMISSION_MODE_DISPLAY_NAMES, UNCONFIRMED_FILE_CHANGES_WARNING } from '../utils/permission-mode'; import { canRestoreSubmittedInput } from './resolve'; import type { SlashCommandHost } from './dispatch'; @@ -39,6 +40,7 @@ type GoalCommandHost = Pick< | 'requireSession' | 'setAppState' | 'showError' + | 'showNotice' | 'showStatus' | 'track' | 'mountEditorReplacement' @@ -454,6 +456,14 @@ async function setPermissionForGoal(host: GoalCommandHost, mode: PermissionMode) return false; } host.setAppState({ permissionMode: mode }); + // The failure-rollback path also comes through here with 'manual'; only warn + // when entering a mode that skips confirmation. + if (mode !== 'manual') { + host.showNotice( + `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, + UNCONFIRMED_FILE_CHANGES_WARNING, + ); + } return true; } diff --git a/apps/kimi-code/src/tui/commands/swarm.ts b/apps/kimi-code/src/tui/commands/swarm.ts index 540aa58604e..544017ef9df 100644 --- a/apps/kimi-code/src/tui/commands/swarm.ts +++ b/apps/kimi-code/src/tui/commands/swarm.ts @@ -10,6 +10,7 @@ import { } from '../components/messages/swarm-markers'; import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; +import { PERMISSION_MODE_DISPLAY_NAMES, UNCONFIRMED_FILE_CHANGES_WARNING } from '../utils/permission-mode'; import type { SlashCommandHost } from './dispatch'; export async function handleSwarmCommand(host: SlashCommandHost, args: string): Promise { @@ -85,6 +86,10 @@ async function setPermissionForSwarm(host: SlashCommandHost, mode: PermissionMod return false; } host.setAppState({ permissionMode: mode }); + host.showNotice( + `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, + UNCONFIRMED_FILE_CHANGES_WARNING, + ); return true; } diff --git a/apps/kimi-code/src/tui/utils/permission-mode.ts b/apps/kimi-code/src/tui/utils/permission-mode.ts index 9adec263708..8a99ddec964 100644 --- a/apps/kimi-code/src/tui/utils/permission-mode.ts +++ b/apps/kimi-code/src/tui/utils/permission-mode.ts @@ -5,3 +5,6 @@ export const PERMISSION_MODE_DISPLAY_NAMES: Readonly ({ appendGoalQueueItem: vi.fn(async () => ({ @@ -375,6 +376,10 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).toHaveBeenCalledWith('auto'); expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'auto' }); + expect(manualHost.showNotice).toHaveBeenCalledWith( + 'Permission mode: Never Ask', + UNCONFIRMED_FILE_CHANGES_WARNING, + ); expect(manualHost.sendNormalUserInput).toHaveBeenCalledWith('Ship feature X'); }); @@ -393,6 +398,7 @@ describe('handleGoalCommand', () => { ); }); expect(s.setPermission).not.toHaveBeenCalled(); + expect(manualHost.showNotice).not.toHaveBeenCalled(); expect(manualHost.sendNormalUserInput).toHaveBeenCalledWith('Ship feature X'); }); @@ -411,6 +417,10 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).toHaveBeenCalledWith('yolo'); expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'yolo' }); + expect(manualHost.showNotice).toHaveBeenCalledWith( + 'Permission mode: Ask When Needed', + UNCONFIRMED_FILE_CHANGES_WARNING, + ); }); it('restores the previous permission mode when the goal fails to start', async () => { @@ -430,6 +440,8 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).toHaveBeenCalledWith('yolo'); expect(manualHost.setAppState).toHaveBeenLastCalledWith({ permissionMode: 'manual' }); + // The rollback to Manual must not emit a second warning notice. + expect(manualHost.showNotice).toHaveBeenCalledTimes(1); }); it('returns the command to the input box when a Manual-mode goal start is cancelled', async () => { diff --git a/apps/kimi-code/test/tui/commands/swarm.test.ts b/apps/kimi-code/test/tui/commands/swarm.test.ts index a4d00ea30ac..c069e10dce0 100644 --- a/apps/kimi-code/test/tui/commands/swarm.test.ts +++ b/apps/kimi-code/test/tui/commands/swarm.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import { handleSwarmCommand } from '#/tui/commands/index'; import type { SlashCommandHost } from '#/tui/commands/dispatch'; import { currentTheme } from '#/tui/theme'; +import { UNCONFIRMED_FILE_CHANGES_WARNING } from '#/tui/utils/permission-mode'; const ENTER = '\r'; const ESCAPE = '\u001B'; @@ -44,6 +45,7 @@ function makeHost( requireSession: () => session, setAppState: vi.fn((patch: Record) => Object.assign(host.state.appState, patch)), showError: vi.fn(), + showNotice: vi.fn(), showStatus: vi.fn(), mountEditorReplacement: vi.fn(), restoreEditor: vi.fn(), @@ -232,6 +234,10 @@ describe('handleSwarmCommand', () => { expect(session.setSwarmMode).toHaveBeenCalledTimes(1); expect(host.setAppState).toHaveBeenCalledWith({ permissionMode: 'auto' }); expect(host.setAppState).toHaveBeenCalledWith({ swarmMode: true }); + expect(host.showNotice).toHaveBeenCalledWith( + 'Permission mode: Never Ask', + UNCONFIRMED_FILE_CHANGES_WARNING, + ); expect(host.state.swarmModeEntry).toBe('task'); expectSwarmMarker(host, 'Swarm activated'); }); @@ -251,6 +257,7 @@ describe('handleSwarmCommand', () => { expect(session.setPermission).not.toHaveBeenCalled(); expect(session.setSwarmMode).toHaveBeenCalledWith(true, 'task'); expect(session.setSwarmMode).toHaveBeenCalledTimes(1); + expect(host.showNotice).not.toHaveBeenCalled(); expect(host.state.swarmModeEntry).toBe('task'); expectSwarmMarker(host, 'Swarm activated'); }); @@ -271,6 +278,10 @@ describe('handleSwarmCommand', () => { expect(session.setSwarmMode).toHaveBeenCalledTimes(1); expect(host.setAppState).toHaveBeenCalledWith({ permissionMode: 'yolo' }); expect(host.setAppState).toHaveBeenCalledWith({ swarmMode: true }); + expect(host.showNotice).toHaveBeenCalledWith( + 'Permission mode: Ask When Needed', + UNCONFIRMED_FILE_CHANGES_WARNING, + ); expect(host.state.swarmModeEntry).toBe('task'); expectSwarmMarker(host, 'Swarm activated'); }); From 7cdb485465ef13b0170d07e16a3a9a5e9fd344d0 Mon Sep 17 00:00:00 2001 From: 7Sageer <125936732+7Sageer@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:11:00 +0800 Subject: [PATCH 4/8] fix(kimi-code): indent every line of multi-line notice details --- .../src/tui/components/messages/status-message.ts | 11 +++++++++-- .../test/tui/components/messages/notice.test.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/kimi-code/src/tui/components/messages/status-message.ts b/apps/kimi-code/src/tui/components/messages/status-message.ts index f88c1861ba1..5a0523f58f0 100644 --- a/apps/kimi-code/src/tui/components/messages/status-message.ts +++ b/apps/kimi-code/src/tui/components/messages/status-message.ts @@ -57,7 +57,7 @@ export class NoticeMessageComponent extends Container { this.titleText = new Text(` ${currentTheme.fg('textStrong', title)}`, 0, 0); this.addChild(this.titleText); if (detail !== undefined && detail.length > 0) { - this.detailText = new Text(` ${currentTheme.fg('textDim', detail)}`, 0, 0); + this.detailText = new Text(this.renderDetail(detail), 0, 0); this.addChild(this.detailText); } } @@ -65,8 +65,15 @@ export class NoticeMessageComponent extends Container { override invalidate(): void { this.titleText.setText(` ${currentTheme.fg('textStrong', this.title)}`); if (this.detailText !== undefined && this.detail !== undefined) { - this.detailText.setText(` ${currentTheme.fg('textDim', this.detail)}`); + this.detailText.setText(this.renderDetail(this.detail)); } super.invalidate(); } + + // Indent every line, not just the first. The `detail` may be multi-line; + // prefixing the whole string once would only indent the first line and leave + // the rest at column 0 (same handling as StatusMessageComponent). + private renderDetail(detail: string): string { + return currentTheme.fg('textDim', detail).split('\n').map((line) => ` ${line}`).join('\n'); + } } diff --git a/apps/kimi-code/test/tui/components/messages/notice.test.ts b/apps/kimi-code/test/tui/components/messages/notice.test.ts index 09f727556a7..b915a2b3b7a 100644 --- a/apps/kimi-code/test/tui/components/messages/notice.test.ts +++ b/apps/kimi-code/test/tui/components/messages/notice.test.ts @@ -23,6 +23,18 @@ describe('NoticeComponent', () => { expect(lines[1]).toContain('Plan mode: ON'); expect(lines[2]).toContain('Plan will be created here: /tmp/plans/test-plan.md'); }); + + it('indents every line of a multi-line detail, not just the first', () => { + const component = new NoticeMessageComponent('Title', 'First line.\nSecond line.'); + + const lines = component.render(120).map((line) => strip(line)); + const titleColumn = lines.find((line) => line.includes('Title'))?.indexOf('Title'); + const firstColumn = lines.find((line) => line.includes('First line.'))?.indexOf('First line.'); + const secondColumn = lines.find((line) => line.includes('Second line.'))?.indexOf('Second line.'); + expect(titleColumn).toBeDefined(); + expect(firstColumn).toBe(titleColumn); + expect(secondColumn).toBe(titleColumn); + }); }); describe('CronMessageComponent', () => { From 08f1eb3067cf4ad964130b5cf714b107206444cf Mon Sep 17 00:00:00 2001 From: 7Sageer <125936732+7Sageer@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:07:13 +0800 Subject: [PATCH 5/8] feat(kimi-code): render the permission-mode file-change warning in warning color --- apps/kimi-code/src/tui/commands/config.ts | 13 +++++++---- apps/kimi-code/src/tui/commands/dispatch.ts | 2 +- apps/kimi-code/src/tui/commands/goal.ts | 1 + apps/kimi-code/src/tui/commands/swarm.ts | 1 + .../tui/components/messages/status-message.ts | 22 ++++++++++++++----- .../tui/controllers/session-event-handler.ts | 2 +- apps/kimi-code/src/tui/kimi-tui.ts | 4 ++-- apps/kimi-code/test/tui/commands/goal.test.ts | 2 ++ .../kimi-code/test/tui/commands/swarm.test.ts | 2 ++ .../tui/components/messages/notice.test.ts | 19 ++++++++++++++++ 10 files changed, 54 insertions(+), 14 deletions(-) diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index e3762f6bfc4..7cc778e3861 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -147,7 +147,8 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P host.setAppState({ permissionMode: 'yolo' }); host.showNotice( 'Ask When Needed mode: ON', - `Routine edits and commands run automatically; risky actions, questions, and plans still ask.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.', + UNCONFIRMED_FILE_CHANGES_WARNING, ); return; } @@ -173,7 +174,8 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P host.setAppState({ permissionMode: 'yolo' }); host.showNotice( 'Ask When Needed mode: ON', - `Routine edits and commands run automatically; risky actions, questions, and plans still ask.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.', + UNCONFIRMED_FILE_CHANGES_WARNING, ); } } @@ -199,7 +201,8 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P host.setAppState({ permissionMode: 'auto' }); host.showNotice( 'Never Ask mode: ON', - `Never interrupts you; everything runs and is decided automatically.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + 'Never interrupts you; everything runs and is decided automatically.', + UNCONFIRMED_FILE_CHANGES_WARNING, ); return; } @@ -225,7 +228,8 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P host.setAppState({ permissionMode: 'auto' }); host.showNotice( 'Never Ask mode: ON', - `Never interrupts you; everything runs and is decided automatically.\n${UNCONFIRMED_FILE_CHANGES_WARNING}`, + 'Never interrupts you; everything runs and is decided automatically.', + UNCONFIRMED_FILE_CHANGES_WARNING, ); } } @@ -920,6 +924,7 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod host.setAppState({ permissionMode: mode }); host.showNotice( `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, + undefined, mode === 'manual' ? undefined : UNCONFIRMED_FILE_CHANGES_WARNING, ); } diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index afe3b88be43..f0d60808cab 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -129,7 +129,7 @@ export interface SlashCommandHost { resetLivePane(): void; showError(msg: string): void; showStatus(msg: string, color?: ColorToken): void; - showNotice(title: string, detail?: string): void; + showNotice(title: string, detail?: string, warning?: string): void; appendTranscriptEntry(entry: TranscriptEntry): void; track(event: string, props?: Record): void; mountEditorReplacement(panel: Component & Focusable): void; diff --git a/apps/kimi-code/src/tui/commands/goal.ts b/apps/kimi-code/src/tui/commands/goal.ts index f79230d82a6..5abba4a9e54 100644 --- a/apps/kimi-code/src/tui/commands/goal.ts +++ b/apps/kimi-code/src/tui/commands/goal.ts @@ -461,6 +461,7 @@ async function setPermissionForGoal(host: GoalCommandHost, mode: PermissionMode) if (mode !== 'manual') { host.showNotice( `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, + undefined, UNCONFIRMED_FILE_CHANGES_WARNING, ); } diff --git a/apps/kimi-code/src/tui/commands/swarm.ts b/apps/kimi-code/src/tui/commands/swarm.ts index 544017ef9df..393eba2b680 100644 --- a/apps/kimi-code/src/tui/commands/swarm.ts +++ b/apps/kimi-code/src/tui/commands/swarm.ts @@ -88,6 +88,7 @@ async function setPermissionForSwarm(host: SlashCommandHost, mode: PermissionMod host.setAppState({ permissionMode: mode }); host.showNotice( `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, + undefined, UNCONFIRMED_FILE_CHANGES_WARNING, ); return true; diff --git a/apps/kimi-code/src/tui/components/messages/status-message.ts b/apps/kimi-code/src/tui/components/messages/status-message.ts index 5a0523f58f0..c12c99d69f4 100644 --- a/apps/kimi-code/src/tui/components/messages/status-message.ts +++ b/apps/kimi-code/src/tui/components/messages/status-message.ts @@ -46,34 +46,44 @@ export class StatusMessageComponent extends Container { export class NoticeMessageComponent extends Container { private titleText: Text; private detailText?: Text; + private warningText?: Text; private title: string; private detail?: string; + private warning?: string; - constructor(title: string, detail: string | undefined) { + constructor(title: string, detail: string | undefined, warning?: string) { super(); this.title = title; this.detail = detail; + this.warning = warning; this.addChild(new Spacer(1)); this.titleText = new Text(` ${currentTheme.fg('textStrong', title)}`, 0, 0); this.addChild(this.titleText); if (detail !== undefined && detail.length > 0) { - this.detailText = new Text(this.renderDetail(detail), 0, 0); + this.detailText = new Text(this.renderDetail(detail, 'textDim'), 0, 0); this.addChild(this.detailText); } + if (warning !== undefined && warning.length > 0) { + this.warningText = new Text(this.renderDetail(warning, 'warning'), 0, 0); + this.addChild(this.warningText); + } } override invalidate(): void { this.titleText.setText(` ${currentTheme.fg('textStrong', this.title)}`); if (this.detailText !== undefined && this.detail !== undefined) { - this.detailText.setText(this.renderDetail(this.detail)); + this.detailText.setText(this.renderDetail(this.detail, 'textDim')); + } + if (this.warningText !== undefined && this.warning !== undefined) { + this.warningText.setText(this.renderDetail(this.warning, 'warning')); } super.invalidate(); } - // Indent every line, not just the first. The `detail` may be multi-line; + // Indent every line, not just the first. The text may be multi-line; // prefixing the whole string once would only indent the first line and leave // the rest at column 0 (same handling as StatusMessageComponent). - private renderDetail(detail: string): string { - return currentTheme.fg('textDim', detail).split('\n').map((line) => ` ${line}`).join('\n'); + private renderDetail(text: string, token: ColorToken): string { + return currentTheme.fg(token, text).split('\n').map((line) => ` ${line}`).join('\n'); } } diff --git a/apps/kimi-code/src/tui/controllers/session-event-handler.ts b/apps/kimi-code/src/tui/controllers/session-event-handler.ts index 0d623d01c2b..3b4df6ea3b2 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -103,7 +103,7 @@ export interface SessionEventHost { resetLivePane(): void; showError(msg: string): void; showStatus(msg: string, color?: ColorToken): void; - showNotice(title: string, detail?: string): void; + showNotice(title: string, detail?: string, warning?: string): void; updateActivityPane(): void; track(event: string, props?: Record): void; recordSessionActivity(): void; diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index da68c171c7a..2b458bd6156 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -3220,8 +3220,8 @@ export class KimiTUI { this.state.ui.requestRender(); } - showNotice(title: string, detail?: string): void { - this.state.transcriptContainer.addChild(new NoticeMessageComponent(title, detail)); + showNotice(title: string, detail?: string, warning?: string): void { + this.state.transcriptContainer.addChild(new NoticeMessageComponent(title, detail, warning)); this.state.ui.requestRender(); } diff --git a/apps/kimi-code/test/tui/commands/goal.test.ts b/apps/kimi-code/test/tui/commands/goal.test.ts index 9ca632f1bc0..838b8032930 100644 --- a/apps/kimi-code/test/tui/commands/goal.test.ts +++ b/apps/kimi-code/test/tui/commands/goal.test.ts @@ -378,6 +378,7 @@ describe('handleGoalCommand', () => { expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'auto' }); expect(manualHost.showNotice).toHaveBeenCalledWith( 'Permission mode: Never Ask', + undefined, UNCONFIRMED_FILE_CHANGES_WARNING, ); expect(manualHost.sendNormalUserInput).toHaveBeenCalledWith('Ship feature X'); @@ -419,6 +420,7 @@ describe('handleGoalCommand', () => { expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'yolo' }); expect(manualHost.showNotice).toHaveBeenCalledWith( 'Permission mode: Ask When Needed', + undefined, UNCONFIRMED_FILE_CHANGES_WARNING, ); }); diff --git a/apps/kimi-code/test/tui/commands/swarm.test.ts b/apps/kimi-code/test/tui/commands/swarm.test.ts index c069e10dce0..ef2e3da0174 100644 --- a/apps/kimi-code/test/tui/commands/swarm.test.ts +++ b/apps/kimi-code/test/tui/commands/swarm.test.ts @@ -236,6 +236,7 @@ describe('handleSwarmCommand', () => { expect(host.setAppState).toHaveBeenCalledWith({ swarmMode: true }); expect(host.showNotice).toHaveBeenCalledWith( 'Permission mode: Never Ask', + undefined, UNCONFIRMED_FILE_CHANGES_WARNING, ); expect(host.state.swarmModeEntry).toBe('task'); @@ -280,6 +281,7 @@ describe('handleSwarmCommand', () => { expect(host.setAppState).toHaveBeenCalledWith({ swarmMode: true }); expect(host.showNotice).toHaveBeenCalledWith( 'Permission mode: Ask When Needed', + undefined, UNCONFIRMED_FILE_CHANGES_WARNING, ); expect(host.state.swarmModeEntry).toBe('task'); diff --git a/apps/kimi-code/test/tui/components/messages/notice.test.ts b/apps/kimi-code/test/tui/components/messages/notice.test.ts index b915a2b3b7a..ac24aa7d04a 100644 --- a/apps/kimi-code/test/tui/components/messages/notice.test.ts +++ b/apps/kimi-code/test/tui/components/messages/notice.test.ts @@ -1,4 +1,5 @@ import { visibleWidth } from '@moonshot-ai/pi-tui'; +import chalk from 'chalk'; import { describe, expect, it } from 'vitest'; import { CronMessageComponent } from '#/tui/components/messages/cron-message'; @@ -35,6 +36,24 @@ describe('NoticeComponent', () => { expect(firstColumn).toBe(titleColumn); expect(secondColumn).toBe(titleColumn); }); + + it('renders the warning line in the warning color, aligned with the title', () => { + const previousChalkLevel = chalk.level; + chalk.level = 3; + try { + const component = new NoticeMessageComponent('Title', 'Detail line.', 'Warning line.'); + + const lines = component.render(120); + // darkColors.warning #E8A838 + expect(lines.find((line) => line.includes('Warning line.'))).toContain('38;2;232;168;56'); + const stripped = lines.map((line) => strip(line)); + const titleColumn = stripped.find((line) => line.includes('Title'))?.indexOf('Title'); + const warningColumn = stripped.find((line) => line.includes('Warning line.'))?.indexOf('Warning line.'); + expect(warningColumn).toBe(titleColumn); + } finally { + chalk.level = previousChalkLevel; + } + }); }); describe('CronMessageComponent', () => { From 618b501c02b094591a7fe41639c50461094972e4 Mon Sep 17 00:00:00 2001 From: 7Sageer <125936732+7Sageer@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:17:56 +0800 Subject: [PATCH 6/8] refactor(kimi-code): render the file-change warning via showStatus --- apps/kimi-code/src/tui/commands/config.ts | 37 ++++++------------- apps/kimi-code/src/tui/commands/dispatch.ts | 2 +- apps/kimi-code/src/tui/commands/goal.ts | 7 +--- apps/kimi-code/src/tui/commands/swarm.ts | 7 +--- .../tui/components/messages/status-message.ts | 22 +++-------- .../tui/controllers/session-event-handler.ts | 2 +- apps/kimi-code/src/tui/kimi-tui.ts | 4 +- apps/kimi-code/test/tui/commands/goal.test.ts | 18 ++++----- .../kimi-code/test/tui/commands/swarm.test.ts | 15 +++----- .../tui/components/messages/notice.test.ts | 19 ---------- 10 files changed, 38 insertions(+), 95 deletions(-) diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index 7cc778e3861..d54ff273e24 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -145,11 +145,8 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P } await session?.setPermission('yolo'); host.setAppState({ permissionMode: 'yolo' }); - host.showNotice( - 'Ask When Needed mode: ON', - 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.', - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + host.showNotice('Ask When Needed mode: ON', 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.'); + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); return; } @@ -172,11 +169,8 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P } else { await session?.setPermission('yolo'); host.setAppState({ permissionMode: 'yolo' }); - host.showNotice( - 'Ask When Needed mode: ON', - 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.', - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + host.showNotice('Ask When Needed mode: ON', 'Routine edits and commands run automatically; risky actions, questions, and plans still ask.'); + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); } } @@ -199,11 +193,8 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P } await session?.setPermission('auto'); host.setAppState({ permissionMode: 'auto' }); - host.showNotice( - 'Never Ask mode: ON', - 'Never interrupts you; everything runs and is decided automatically.', - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + host.showNotice('Never Ask mode: ON', 'Never interrupts you; everything runs and is decided automatically.'); + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); return; } @@ -226,11 +217,8 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P } else { await session?.setPermission('auto'); host.setAppState({ permissionMode: 'auto' }); - host.showNotice( - 'Never Ask mode: ON', - 'Never interrupts you; everything runs and is decided automatically.', - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + host.showNotice('Never Ask mode: ON', 'Never interrupts you; everything runs and is decided automatically.'); + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); } } @@ -922,11 +910,10 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod } host.setAppState({ permissionMode: mode }); - host.showNotice( - `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, - undefined, - mode === 'manual' ? undefined : UNCONFIRMED_FILE_CHANGES_WARNING, - ); + host.showNotice(`Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`); + if (mode !== 'manual') { + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); + } } export function showSettingsSelector(host: SlashCommandHost): void { diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index f0d60808cab..afe3b88be43 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -129,7 +129,7 @@ export interface SlashCommandHost { resetLivePane(): void; showError(msg: string): void; showStatus(msg: string, color?: ColorToken): void; - showNotice(title: string, detail?: string, warning?: string): void; + showNotice(title: string, detail?: string): void; appendTranscriptEntry(entry: TranscriptEntry): void; track(event: string, props?: Record): void; mountEditorReplacement(panel: Component & Focusable): void; diff --git a/apps/kimi-code/src/tui/commands/goal.ts b/apps/kimi-code/src/tui/commands/goal.ts index 5abba4a9e54..45ba91296df 100644 --- a/apps/kimi-code/src/tui/commands/goal.ts +++ b/apps/kimi-code/src/tui/commands/goal.ts @@ -459,11 +459,8 @@ async function setPermissionForGoal(host: GoalCommandHost, mode: PermissionMode) // The failure-rollback path also comes through here with 'manual'; only warn // when entering a mode that skips confirmation. if (mode !== 'manual') { - host.showNotice( - `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, - undefined, - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + host.showNotice(`Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`); + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); } return true; } diff --git a/apps/kimi-code/src/tui/commands/swarm.ts b/apps/kimi-code/src/tui/commands/swarm.ts index 393eba2b680..c518e6e6a06 100644 --- a/apps/kimi-code/src/tui/commands/swarm.ts +++ b/apps/kimi-code/src/tui/commands/swarm.ts @@ -86,11 +86,8 @@ async function setPermissionForSwarm(host: SlashCommandHost, mode: PermissionMod return false; } host.setAppState({ permissionMode: mode }); - host.showNotice( - `Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`, - undefined, - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + host.showNotice(`Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`); + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); return true; } diff --git a/apps/kimi-code/src/tui/components/messages/status-message.ts b/apps/kimi-code/src/tui/components/messages/status-message.ts index c12c99d69f4..5a0523f58f0 100644 --- a/apps/kimi-code/src/tui/components/messages/status-message.ts +++ b/apps/kimi-code/src/tui/components/messages/status-message.ts @@ -46,44 +46,34 @@ export class StatusMessageComponent extends Container { export class NoticeMessageComponent extends Container { private titleText: Text; private detailText?: Text; - private warningText?: Text; private title: string; private detail?: string; - private warning?: string; - constructor(title: string, detail: string | undefined, warning?: string) { + constructor(title: string, detail: string | undefined) { super(); this.title = title; this.detail = detail; - this.warning = warning; this.addChild(new Spacer(1)); this.titleText = new Text(` ${currentTheme.fg('textStrong', title)}`, 0, 0); this.addChild(this.titleText); if (detail !== undefined && detail.length > 0) { - this.detailText = new Text(this.renderDetail(detail, 'textDim'), 0, 0); + this.detailText = new Text(this.renderDetail(detail), 0, 0); this.addChild(this.detailText); } - if (warning !== undefined && warning.length > 0) { - this.warningText = new Text(this.renderDetail(warning, 'warning'), 0, 0); - this.addChild(this.warningText); - } } override invalidate(): void { this.titleText.setText(` ${currentTheme.fg('textStrong', this.title)}`); if (this.detailText !== undefined && this.detail !== undefined) { - this.detailText.setText(this.renderDetail(this.detail, 'textDim')); - } - if (this.warningText !== undefined && this.warning !== undefined) { - this.warningText.setText(this.renderDetail(this.warning, 'warning')); + this.detailText.setText(this.renderDetail(this.detail)); } super.invalidate(); } - // Indent every line, not just the first. The text may be multi-line; + // Indent every line, not just the first. The `detail` may be multi-line; // prefixing the whole string once would only indent the first line and leave // the rest at column 0 (same handling as StatusMessageComponent). - private renderDetail(text: string, token: ColorToken): string { - return currentTheme.fg(token, text).split('\n').map((line) => ` ${line}`).join('\n'); + private renderDetail(detail: string): string { + return currentTheme.fg('textDim', detail).split('\n').map((line) => ` ${line}`).join('\n'); } } diff --git a/apps/kimi-code/src/tui/controllers/session-event-handler.ts b/apps/kimi-code/src/tui/controllers/session-event-handler.ts index 3b4df6ea3b2..0d623d01c2b 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -103,7 +103,7 @@ export interface SessionEventHost { resetLivePane(): void; showError(msg: string): void; showStatus(msg: string, color?: ColorToken): void; - showNotice(title: string, detail?: string, warning?: string): void; + showNotice(title: string, detail?: string): void; updateActivityPane(): void; track(event: string, props?: Record): void; recordSessionActivity(): void; diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 2b458bd6156..da68c171c7a 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -3220,8 +3220,8 @@ export class KimiTUI { this.state.ui.requestRender(); } - showNotice(title: string, detail?: string, warning?: string): void { - this.state.transcriptContainer.addChild(new NoticeMessageComponent(title, detail, warning)); + showNotice(title: string, detail?: string): void { + this.state.transcriptContainer.addChild(new NoticeMessageComponent(title, detail)); this.state.ui.requestRender(); } diff --git a/apps/kimi-code/test/tui/commands/goal.test.ts b/apps/kimi-code/test/tui/commands/goal.test.ts index 838b8032930..78a18433f60 100644 --- a/apps/kimi-code/test/tui/commands/goal.test.ts +++ b/apps/kimi-code/test/tui/commands/goal.test.ts @@ -376,11 +376,8 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).toHaveBeenCalledWith('auto'); expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'auto' }); - expect(manualHost.showNotice).toHaveBeenCalledWith( - 'Permission mode: Never Ask', - undefined, - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + expect(manualHost.showNotice).toHaveBeenCalledWith('Permission mode: Never Ask'); + expect(manualHost.showStatus).toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); expect(manualHost.sendNormalUserInput).toHaveBeenCalledWith('Ship feature X'); }); @@ -400,6 +397,7 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).not.toHaveBeenCalled(); expect(manualHost.showNotice).not.toHaveBeenCalled(); + expect(manualHost.showStatus).not.toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); expect(manualHost.sendNormalUserInput).toHaveBeenCalledWith('Ship feature X'); }); @@ -418,11 +416,8 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).toHaveBeenCalledWith('yolo'); expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'yolo' }); - expect(manualHost.showNotice).toHaveBeenCalledWith( - 'Permission mode: Ask When Needed', - undefined, - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + expect(manualHost.showNotice).toHaveBeenCalledWith('Permission mode: Ask When Needed'); + expect(manualHost.showStatus).toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); }); it('restores the previous permission mode when the goal fails to start', async () => { @@ -442,8 +437,9 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).toHaveBeenCalledWith('yolo'); expect(manualHost.setAppState).toHaveBeenLastCalledWith({ permissionMode: 'manual' }); - // The rollback to Manual must not emit a second warning notice. + // The rollback to Manual must not emit a second warning. expect(manualHost.showNotice).toHaveBeenCalledTimes(1); + expect(manualHost.showStatus).toHaveBeenCalledTimes(1); }); it('returns the command to the input box when a Manual-mode goal start is cancelled', async () => { diff --git a/apps/kimi-code/test/tui/commands/swarm.test.ts b/apps/kimi-code/test/tui/commands/swarm.test.ts index ef2e3da0174..ad5ef175c0a 100644 --- a/apps/kimi-code/test/tui/commands/swarm.test.ts +++ b/apps/kimi-code/test/tui/commands/swarm.test.ts @@ -234,11 +234,8 @@ describe('handleSwarmCommand', () => { expect(session.setSwarmMode).toHaveBeenCalledTimes(1); expect(host.setAppState).toHaveBeenCalledWith({ permissionMode: 'auto' }); expect(host.setAppState).toHaveBeenCalledWith({ swarmMode: true }); - expect(host.showNotice).toHaveBeenCalledWith( - 'Permission mode: Never Ask', - undefined, - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + expect(host.showNotice).toHaveBeenCalledWith('Permission mode: Never Ask'); + expect(host.showStatus).toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); expect(host.state.swarmModeEntry).toBe('task'); expectSwarmMarker(host, 'Swarm activated'); }); @@ -259,6 +256,7 @@ describe('handleSwarmCommand', () => { expect(session.setSwarmMode).toHaveBeenCalledWith(true, 'task'); expect(session.setSwarmMode).toHaveBeenCalledTimes(1); expect(host.showNotice).not.toHaveBeenCalled(); + expect(host.showStatus).not.toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); expect(host.state.swarmModeEntry).toBe('task'); expectSwarmMarker(host, 'Swarm activated'); }); @@ -279,11 +277,8 @@ describe('handleSwarmCommand', () => { expect(session.setSwarmMode).toHaveBeenCalledTimes(1); expect(host.setAppState).toHaveBeenCalledWith({ permissionMode: 'yolo' }); expect(host.setAppState).toHaveBeenCalledWith({ swarmMode: true }); - expect(host.showNotice).toHaveBeenCalledWith( - 'Permission mode: Ask When Needed', - undefined, - UNCONFIRMED_FILE_CHANGES_WARNING, - ); + expect(host.showNotice).toHaveBeenCalledWith('Permission mode: Ask When Needed'); + expect(host.showStatus).toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); expect(host.state.swarmModeEntry).toBe('task'); expectSwarmMarker(host, 'Swarm activated'); }); diff --git a/apps/kimi-code/test/tui/components/messages/notice.test.ts b/apps/kimi-code/test/tui/components/messages/notice.test.ts index ac24aa7d04a..b915a2b3b7a 100644 --- a/apps/kimi-code/test/tui/components/messages/notice.test.ts +++ b/apps/kimi-code/test/tui/components/messages/notice.test.ts @@ -1,5 +1,4 @@ import { visibleWidth } from '@moonshot-ai/pi-tui'; -import chalk from 'chalk'; import { describe, expect, it } from 'vitest'; import { CronMessageComponent } from '#/tui/components/messages/cron-message'; @@ -36,24 +35,6 @@ describe('NoticeComponent', () => { expect(firstColumn).toBe(titleColumn); expect(secondColumn).toBe(titleColumn); }); - - it('renders the warning line in the warning color, aligned with the title', () => { - const previousChalkLevel = chalk.level; - chalk.level = 3; - try { - const component = new NoticeMessageComponent('Title', 'Detail line.', 'Warning line.'); - - const lines = component.render(120); - // darkColors.warning #E8A838 - expect(lines.find((line) => line.includes('Warning line.'))).toContain('38;2;232;168;56'); - const stripped = lines.map((line) => strip(line)); - const titleColumn = stripped.find((line) => line.includes('Title'))?.indexOf('Title'); - const warningColumn = stripped.find((line) => line.includes('Warning line.'))?.indexOf('Warning line.'); - expect(warningColumn).toBe(titleColumn); - } finally { - chalk.level = previousChalkLevel; - } - }); }); describe('CronMessageComponent', () => { From c7e091a091d4e4cf031f8895848565f3d3927bb9 Mon Sep 17 00:00:00 2001 From: 7Sageer <125936732+7Sageer@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:25:33 +0800 Subject: [PATCH 7/8] fix(kimi-code): defer the goal permission-switch notice until the goal starts --- apps/kimi-code/src/tui/commands/goal.ts | 14 ++++++++------ apps/kimi-code/test/tui/commands/goal.test.ts | 7 ++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/kimi-code/src/tui/commands/goal.ts b/apps/kimi-code/src/tui/commands/goal.ts index 45ba91296df..8208788b368 100644 --- a/apps/kimi-code/src/tui/commands/goal.ts +++ b/apps/kimi-code/src/tui/commands/goal.ts @@ -445,6 +445,14 @@ async function startGoalWithPermission( // previous mode so the session is not left more permissive than before. if (!started && switched) { await setPermissionForGoal(host, previousMode); + return; + } + // Announce the switch only once the goal actually starts: shown earlier, a + // failed creation would leave a stale permissive-mode notice in the + // transcript even though the rollback above restored the previous mode. + if (switched) { + host.showNotice(`Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[choice]}`); + host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); } } @@ -456,12 +464,6 @@ async function setPermissionForGoal(host: GoalCommandHost, mode: PermissionMode) return false; } host.setAppState({ permissionMode: mode }); - // The failure-rollback path also comes through here with 'manual'; only warn - // when entering a mode that skips confirmation. - if (mode !== 'manual') { - host.showNotice(`Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`); - host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); - } return true; } diff --git a/apps/kimi-code/test/tui/commands/goal.test.ts b/apps/kimi-code/test/tui/commands/goal.test.ts index 78a18433f60..30cd10bc4b9 100644 --- a/apps/kimi-code/test/tui/commands/goal.test.ts +++ b/apps/kimi-code/test/tui/commands/goal.test.ts @@ -437,9 +437,10 @@ describe('handleGoalCommand', () => { }); expect(s.setPermission).toHaveBeenCalledWith('yolo'); expect(manualHost.setAppState).toHaveBeenLastCalledWith({ permissionMode: 'manual' }); - // The rollback to Manual must not emit a second warning. - expect(manualHost.showNotice).toHaveBeenCalledTimes(1); - expect(manualHost.showStatus).toHaveBeenCalledTimes(1); + // The permissive-mode notice is deferred until the goal starts, so a failed + // start leaves no stale notice behind. + expect(manualHost.showNotice).not.toHaveBeenCalled(); + expect(manualHost.showStatus).not.toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning'); }); it('returns the command to the input box when a Manual-mode goal start is cancelled', async () => { From 65b72d66ddeccdf155d69544dbb28e944c59c063 Mon Sep 17 00:00:00 2001 From: 7Sageer <125936732+7Sageer@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:33:07 +0800 Subject: [PATCH 8/8] refactor(kimi-code): move the file-change warning copy into the TUI constant directory --- apps/kimi-code/src/tui/commands/config.ts | 4 ++-- apps/kimi-code/src/tui/commands/goal.ts | 4 ++-- apps/kimi-code/src/tui/commands/swarm.ts | 4 ++-- apps/kimi-code/src/tui/constant/kimi-tui.ts | 2 ++ apps/kimi-code/src/tui/utils/permission-mode.ts | 3 --- apps/kimi-code/test/tui/commands/goal.test.ts | 2 +- apps/kimi-code/test/tui/commands/swarm.test.ts | 2 +- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index d54ff273e24..3e9fa21660d 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -24,9 +24,9 @@ import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update- import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config'; import type { ThemeName } from '#/tui/theme'; import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme'; -import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { NO_ACTIVE_SESSION_MESSAGE, UNCONFIRMED_FILE_CHANGES_WARNING } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; -import { PERMISSION_MODE_DISPLAY_NAMES, UNCONFIRMED_FILE_CHANGES_WARNING } from '../utils/permission-mode'; +import { PERMISSION_MODE_DISPLAY_NAMES } from '../utils/permission-mode'; import { thinkingEffortToConfig } from '../utils/thinking-config'; import { showUsage } from './info'; import { setExperimentalFeatures } from './experimental-flags'; diff --git a/apps/kimi-code/src/tui/commands/goal.ts b/apps/kimi-code/src/tui/commands/goal.ts index 8208788b368..c6f23c1f555 100644 --- a/apps/kimi-code/src/tui/commands/goal.ts +++ b/apps/kimi-code/src/tui/commands/goal.ts @@ -15,7 +15,7 @@ import { GoalStatusMessageComponent, UpcomingGoalAddedMessageComponent, } from '../components/messages/goal-panel'; -import { LLM_NOT_SET_MESSAGE } from '../constant/kimi-tui'; +import { LLM_NOT_SET_MESSAGE, UNCONFIRMED_FILE_CHANGES_WARNING } from '../constant/kimi-tui'; import { appendGoalQueueItem, moveGoalQueueItem, @@ -25,7 +25,7 @@ import { type GoalQueueSnapshot, } from '../goal-queue-store'; import { formatErrorMessage } from '../utils/event-payload'; -import { PERMISSION_MODE_DISPLAY_NAMES, UNCONFIRMED_FILE_CHANGES_WARNING } from '../utils/permission-mode'; +import { PERMISSION_MODE_DISPLAY_NAMES } from '../utils/permission-mode'; import { canRestoreSubmittedInput } from './resolve'; import type { SlashCommandHost } from './dispatch'; diff --git a/apps/kimi-code/src/tui/commands/swarm.ts b/apps/kimi-code/src/tui/commands/swarm.ts index c518e6e6a06..bed78566db5 100644 --- a/apps/kimi-code/src/tui/commands/swarm.ts +++ b/apps/kimi-code/src/tui/commands/swarm.ts @@ -8,9 +8,9 @@ import { SwarmModeMarkerComponent, type SwarmModeMarkerState, } from '../components/messages/swarm-markers'; -import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; +import { LLM_NOT_SET_MESSAGE, NO_ACTIVE_SESSION_MESSAGE, UNCONFIRMED_FILE_CHANGES_WARNING } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; -import { PERMISSION_MODE_DISPLAY_NAMES, UNCONFIRMED_FILE_CHANGES_WARNING } from '../utils/permission-mode'; +import { PERMISSION_MODE_DISPLAY_NAMES } from '../utils/permission-mode'; import type { SlashCommandHost } from './dispatch'; export async function handleSwarmCommand(host: SlashCommandHost, args: string): Promise { diff --git a/apps/kimi-code/src/tui/constant/kimi-tui.ts b/apps/kimi-code/src/tui/constant/kimi-tui.ts index 45c77d177b8..9d9f0ba9479 100644 --- a/apps/kimi-code/src/tui/constant/kimi-tui.ts +++ b/apps/kimi-code/src/tui/constant/kimi-tui.ts @@ -4,6 +4,8 @@ export { DEFAULT_OAUTH_PROVIDER_NAME, OAUTH_LOGIN_REQUIRED_CODE, PRODUCT_NAME } export const LLM_NOT_SET_MESSAGE = 'LLM not set, send "/login" to login'; export const NO_ACTIVE_SESSION_MESSAGE = 'No active session. Send /login to login.'; +export const UNCONFIRMED_FILE_CHANGES_WARNING = + 'In this mode, Kimi Code can modify or delete files without your confirmation'; export const CTRL_D_HINT = 'Press Ctrl+D again to exit'; export const CTRL_C_HINT = 'Press Ctrl+C again to exit'; export const MAIN_AGENT_ID = 'main'; diff --git a/apps/kimi-code/src/tui/utils/permission-mode.ts b/apps/kimi-code/src/tui/utils/permission-mode.ts index 8a99ddec964..9adec263708 100644 --- a/apps/kimi-code/src/tui/utils/permission-mode.ts +++ b/apps/kimi-code/src/tui/utils/permission-mode.ts @@ -5,6 +5,3 @@ export const PERMISSION_MODE_DISPLAY_NAMES: Readonly ({ appendGoalQueueItem: vi.fn(async () => ({ diff --git a/apps/kimi-code/test/tui/commands/swarm.test.ts b/apps/kimi-code/test/tui/commands/swarm.test.ts index ad5ef175c0a..781f0ee8194 100644 --- a/apps/kimi-code/test/tui/commands/swarm.test.ts +++ b/apps/kimi-code/test/tui/commands/swarm.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import { handleSwarmCommand } from '#/tui/commands/index'; import type { SlashCommandHost } from '#/tui/commands/dispatch'; import { currentTheme } from '#/tui/theme'; -import { UNCONFIRMED_FILE_CHANGES_WARNING } from '#/tui/utils/permission-mode'; +import { UNCONFIRMED_FILE_CHANGES_WARNING } from '#/tui/constant/kimi-tui'; const ENTER = '\r'; const ESCAPE = '\u001B';