Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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/permission-mode-file-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Show a warning after switching to Ask When Needed or Never Ask mode.
9 changes: 8 additions & 1 deletion apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -146,6 +146,7 @@ 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.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
return;
}

Expand All @@ -169,6 +170,7 @@ 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.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
}
}

Expand All @@ -192,6 +194,7 @@ 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.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
return;
}

Expand All @@ -215,6 +218,7 @@ 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.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
}
}

Expand Down Expand Up @@ -907,6 +911,9 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod

host.setAppState({ permissionMode: mode });
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 {
Expand Down
8 changes: 8 additions & 0 deletions apps/kimi-code/src/tui/commands/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -39,6 +40,7 @@ type GoalCommandHost = Pick<
| 'requireSession'
| 'setAppState'
| 'showError'
| 'showNotice'
| 'showStatus'
| 'track'
| 'mountEditorReplacement'
Expand Down Expand Up @@ -454,6 +456,12 @@ 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');

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 Report the permission rollback after a failed goal start

When a user starts /goal from Always Ask, selects a permissive mode, and goal creation subsequently fails (for example, because a goal already exists), this notice and warning remain in the transcript even though startGoalWithPermission restores the session to manual. Because the rollback calls this helper with manual, the conditional emits nothing to correct the displayed mode, leaving the user with a persistent message that inaccurately describes their current permissions; emit a matching Always Ask rollback notice or defer the permissive-mode notice until the goal successfully starts.

Useful? React with 👍 / 👎.

}
return true;
}

Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-code/src/tui/commands/swarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand Down Expand Up @@ -85,6 +86,8 @@ async function setPermissionForSwarm(host: SlashCommandHost, mode: PermissionMod
return false;
}
host.setAppState({ permissionMode: mode });
host.showNotice(`Permission mode: ${PERMISSION_MODE_DISPLAY_NAMES[mode]}`);
host.showStatus(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
return true;
}

Expand Down
11 changes: 9 additions & 2 deletions apps/kimi-code/src/tui/components/messages/status-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,23 @@ 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);
}
}

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');
}
}
3 changes: 3 additions & 0 deletions apps/kimi-code/src/tui/utils/permission-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ export const PERMISSION_MODE_DISPLAY_NAMES: Readonly<Record<PermissionMode, stri
yolo: 'Ask When Needed',
auto: 'Never Ask',
};

export const UNCONFIRMED_FILE_CHANGES_WARNING =
'In this mode, Kimi Code can modify or delete files without your confirmation';

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 Move the warning copy into the TUI constant directory

Move UNCONFIRMED_FILE_CHANGES_WARNING to src/tui/constant/ and import it from there. This is shared TUI copy used by multiple command modules, but it is currently declared under utils, contrary to the package rule that constants belong in the corresponding constant directory.

AGENTS.md reference: apps/kimi-code/AGENTS.md:L51-L51

Useful? React with 👍 / 👎.

10 changes: 10 additions & 0 deletions apps/kimi-code/test/tui/commands/goal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '#/tui/goal-queue-store';
import type { SlashCommandHost } from '#/tui/commands/dispatch';
import { getBuiltInPalette } from '#/tui/theme';
import { UNCONFIRMED_FILE_CHANGES_WARNING } from '#/tui/utils/permission-mode';

vi.mock('#/tui/goal-queue-store', () => ({
appendGoalQueueItem: vi.fn(async () => ({
Expand Down Expand Up @@ -375,6 +376,8 @@ describe('handleGoalCommand', () => {
});
expect(s.setPermission).toHaveBeenCalledWith('auto');
expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'auto' });
expect(manualHost.showNotice).toHaveBeenCalledWith('Permission mode: Never Ask');
expect(manualHost.showStatus).toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
expect(manualHost.sendNormalUserInput).toHaveBeenCalledWith('Ship feature X');
});

Expand All @@ -393,6 +396,8 @@ 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');
});

Expand All @@ -411,6 +416,8 @@ describe('handleGoalCommand', () => {
});
expect(s.setPermission).toHaveBeenCalledWith('yolo');
expect(manualHost.setAppState).toHaveBeenCalledWith({ permissionMode: 'yolo' });
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 () => {
Expand All @@ -430,6 +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.
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 () => {
Expand Down
8 changes: 8 additions & 0 deletions apps/kimi-code/test/tui/commands/swarm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +45,7 @@ function makeHost(
requireSession: () => session,
setAppState: vi.fn((patch: Record<string, unknown>) => Object.assign(host.state.appState, patch)),
showError: vi.fn(),
showNotice: vi.fn(),
showStatus: vi.fn(),
mountEditorReplacement: vi.fn(),
restoreEditor: vi.fn(),
Expand Down Expand Up @@ -232,6 +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');
expect(host.showStatus).toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
expect(host.state.swarmModeEntry).toBe('task');
expectSwarmMarker(host, 'Swarm activated');
});
Expand All @@ -251,6 +255,8 @@ 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.showStatus).not.toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
expect(host.state.swarmModeEntry).toBe('task');
expectSwarmMarker(host, 'Swarm activated');
});
Expand All @@ -271,6 +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');
expect(host.showStatus).toHaveBeenCalledWith(UNCONFIRMED_FILE_CHANGES_WARNING, 'warning');
expect(host.state.swarmModeEntry).toBe('task');
expectSwarmMarker(host, 'Swarm activated');
});
Expand Down
12 changes: 12 additions & 0 deletions apps/kimi-code/test/tui/components/messages/notice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
6 changes: 6 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 @@ -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());
});
Expand Down Expand Up @@ -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');
});

Expand Down
Loading