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
7 changes: 7 additions & 0 deletions .changeset/default-swarm-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code-sdk": minor
"@moonshot-ai/kimi-code": minor
---

Add configurable default swarm mode via `default_swarm_mode` in config.toml and `--swarm` / `--no-swarm` CLI flags.
6 changes: 6 additions & 0 deletions .changeset/fix-swarm-mode-cli-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---

Fix default swarm mode handling: preserve explicit `--no-swarm`, reapply `--swarm` after session replay, and persist `default_swarm_mode` to the config file.
5 changes: 4 additions & 1 deletion apps/kimi-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export function createProgram(
)
.addOption(new Option('--yes').hideHelp().default(false))
.addOption(new Option('--auto-approve').hideHelp().default(false))
.option('--plan', 'Start in plan mode.', false);
.option('--plan', 'Start in plan mode.', false)
.option('--swarm', 'Start in swarm mode.')
.option('--no-swarm', 'Do not start in swarm mode.');

registerExportCommand(program);
registerProviderCommand(program);
Expand Down Expand Up @@ -115,6 +117,7 @@ export function createProgram(
yolo: yoloValue,
auto: autoValue,
plan: raw['plan'] as boolean,
swarm: raw['swarm'] as boolean | undefined,
model: raw['model'] as string | undefined,
outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'],
prompt: raw['prompt'] as string | undefined,
Expand Down
4 changes: 4 additions & 0 deletions apps/kimi-code/src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface CLIOptions {
yolo: boolean;
auto: boolean;
plan: boolean;
swarm: boolean | undefined;
model: string | undefined;
outputFormat: PromptOutputFormat | undefined;
prompt: string | undefined;
Expand Down Expand Up @@ -46,6 +47,9 @@ export function validateOptions(opts: CLIOptions): ValidatedOptions {
if (promptMode && opts.plan) {
throw new OptionConflictError('Cannot combine --prompt with --plan.');
}
if (promptMode && opts.swarm) {
throw new OptionConflictError('Cannot combine --prompt with --swarm.');
}
if (promptMode && opts.session === '') {
throw new OptionConflictError('Cannot use --session without an id in prompt mode.');
}
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export async function runShell(
startupNotice: configWarning,
migrationPlan,
migrateOnly: runOptions.migrateOnly,
defaultSwarmMode: config.defaultSwarmMode,
});

initializeCliTelemetry({
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
86 changes: 77 additions & 9 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { CompactionComponent } from './components/dialogs/compaction';
import { HelpPanelComponent } from './components/dialogs/help-panel';
import { QuestionDialogComponent } from './components/dialogs/question-dialog';
import { SessionPickerComponent } from './components/dialogs/session-picker';
import { SwarmStartPermissionPromptComponent } from './components/dialogs/swarm-start-permission-prompt';
import {
FileMentionProvider,
type SlashAutocompleteCommand,
Expand All @@ -70,6 +71,7 @@ import {
GoalSetMessageComponent,
} from './components/messages/goal-panel';
import { SkillActivationComponent } from './components/messages/skill-activation';
import { SwarmModeMarkerComponent } from './components/messages/swarm-markers';
import {
NoticeMessageComponent,
StatusMessageComponent,
Expand Down Expand Up @@ -150,6 +152,8 @@ export interface KimiTUIStartupInput {
readonly migrationPlan?: MigrationPlan | null;
/** When true, run only the migration screen, then exit (the `kimi migrate` command). */
readonly migrateOnly?: boolean;
/** Default swarm mode from config.toml; CLI flags override this. */
readonly defaultSwarmMode?: boolean;
}

type EffectiveActivityPaneMode = ActivityPaneMode | 'idle' | 'session';
Expand All @@ -160,13 +164,14 @@ function createInitialAppState(input: KimiTUIStartupInput): AppState {
: input.cliOptions.yolo
? 'yolo'
: 'manual';
const startupSwarm = input.cliOptions.swarm ?? input.defaultSwarmMode ?? false;
return {
model: '',
workDir: input.workDir,
sessionId: '',
permissionMode: startupPermission,
planMode: input.cliOptions.plan,
swarmMode: false,
swarmMode: startupSwarm,
thinking: false,
contextUsage: 0,
contextTokens: 0,
Expand Down Expand Up @@ -260,6 +265,7 @@ export class KimiTUI {
yolo: startupInput.cliOptions.yolo,
auto: startupInput.cliOptions.auto,
plan: startupInput.cliOptions.plan,
swarm: startupInput.cliOptions.swarm,
model: startupInput.cliOptions.model,
startupNotice: startupInput.startupNotice,
},
Expand Down Expand Up @@ -500,7 +506,7 @@ export class KimiTUI {
}
if (shouldReplayHistory) {
await this.sessionReplay.hydrateFromReplay(this.requireSession());
this.applyStartupPermissionAndPlanToAppState();
this.applyStartupPermissionPlanAndSwarmToAppState();
}
const resumeState = this.session?.getResumeState();
if (resumeState?.warning !== undefined) {
Expand All @@ -514,6 +520,58 @@ export class KimiTUI {
this.updateTerminalTitle();
}
void this.refreshSkillCommands(this.session);
if (!shouldReplayHistory) {
void this.promptForSwarmPermissionIfNeeded();
}
}

private async promptForSwarmPermissionIfNeeded(): Promise<void> {
if (!this.state.appState.swarmMode || this.state.appState.permissionMode !== 'manual') {
return;
}
const session = this.session;
if (session === undefined) return;

this.deferUserMessages = true;
const restore = (): void => {
this.deferUserMessages = false;
this.restoreEditor();
};

this.mountEditorReplacement(
new SwarmStartPermissionPromptComponent({
onSelect: (choice) => {
restore();
if (choice === 'auto' || choice === 'yolo') {
void (async () => {
try {
await session.setPermission(choice);
} catch (error) {
this.showError(`Failed to set permission mode: ${formatErrorMessage(error)}`);
await this.disableStartupSwarmMode(session);
return;
}
this.setAppState({ permissionMode: choice });
})();
}
},
onCancel: () => {
restore();
void this.disableStartupSwarmMode(session);
},
}),
);
}

private async disableStartupSwarmMode(session: Session): Promise<void> {
try {
await session.setSwarmMode(false, 'manual');
} catch (error) {
this.showError(`Failed to disable swarm mode: ${formatErrorMessage(error)}`);
}
this.setAppState({ swarmMode: false });
this.state.transcriptContainer.addChild(new SwarmModeMarkerComponent('inactive'));
this.state.ui.requestRender();
}

private async showTmuxKeyboardWarningIfNeeded(): Promise<void> {
Expand All @@ -537,6 +595,7 @@ export class KimiTUI {
model: startup.model,
permission: startup.auto ? 'auto' : startup.yolo ? 'yolo' : undefined,
planMode: startup.plan ? true : undefined,
swarmMode: this.state.appState.swarmMode,
};

try {
Expand Down Expand Up @@ -604,7 +663,7 @@ export class KimiTUI {
}
await this.setSession(session);
await this.syncRuntimeState(session);
this.applyStartupPermissionAndPlanToAppState();
this.applyStartupPermissionPlanAndSwarmToAppState();
this.state.startupState = 'ready';
return shouldReplayHistory;
}
Expand Down Expand Up @@ -1090,10 +1149,10 @@ export class KimiTUI {
});
}

// Apply --auto/--yolo/--plan startup flags to a resumed session. The resumed
// session may already be in plan mode from its persisted records, and
// re-entering plan mode throws, so only enable it when it is not active yet.
// setPermission is idempotent and needs no such guard.
// Apply --auto/--yolo/--plan/--swarm startup flags to a resumed session. The
// resumed session may already be in plan/swarm mode from its persisted
// records, and re-entering plan mode throws, so only enable it when it is not
// active yet. setPermission is idempotent and needs no such guard.
private async applyStartupModesToResumedSession(session: Session): Promise<void> {
const { startup } = this.options;
if (startup.auto) {
Expand All @@ -1107,12 +1166,18 @@ export class KimiTUI {
await session.setPlanMode(true);
}
}
if (startup.swarm) {
const status = await session.getStatus();
if (!status.swarmMode) {
await session.setSwarmMode(true, 'manual');

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 Reapply resumed --swarm after replay hydration

When resuming with --swarm, this enables swarm in the runtime before syncRuntimeState, but finishStartup then calls sessionReplay.hydrateFromReplay, which hydrates the stale resume snapshot captured before this call and can set appState.swarmMode back to false. The existing post-replay reapply helper only restores permission and plan flags, so the footer/UI can show swarm off while the session is actually active until another status event arrives. Include swarm in the post-replay app-state reapply path.

Useful? React with 👍 / 👎.

}
}
}

// Re-apply startup flags that the user explicitly passed on the command line.
// syncRuntimeState and session-replay hydration can both read stale persisted
// values, so this guarantees the footer reflects the CLI intent.
private applyStartupPermissionAndPlanToAppState(): void {
private applyStartupPermissionPlanAndSwarmToAppState(): void {
const { startup } = this.options;
if (startup.auto) {
this.setAppState({ permissionMode: 'auto' });
Expand All @@ -1122,6 +1187,9 @@ export class KimiTUI {
if (startup.plan) {
this.setAppState({ planMode: true });
}
if (startup.swarm) {
this.setAppState({ swarmMode: true });
}
}

// Plan mode is set by createSession — do not re-enter it here.
Expand Down Expand Up @@ -1895,7 +1963,7 @@ export class KimiTUI {
}
if (options.applyStartupModes === true) {
await this.applyStartupModesToResumedSession(this.requireSession());
this.applyStartupPermissionAndPlanToAppState();
this.applyStartupPermissionPlanAndSwarmToAppState();
}
this.hideSessionPicker();
})
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 @@ -192,6 +192,7 @@ export interface TUIStartupOptions {
readonly yolo: boolean;
readonly auto: boolean;
readonly plan: boolean;
readonly swarm?: boolean;
readonly model?: string;
readonly startupNotice?: string;
}
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ function defaultOpts(): CLIOptions {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
20 changes: 20 additions & 0 deletions apps/kimi-code/test/cli/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ describe('CLI options parsing', () => {
});
});

describe('--swarm / --no-swarm', () => {
it('sets swarm mode flag with --swarm', () => {
expect(parse(['--swarm']).swarm).toBe(true);
});

it('clears swarm mode flag with --no-swarm', () => {
expect(parse(['--no-swarm']).swarm).toBe(false);
});

it('leaves swarm mode unspecified when the flag is absent', () => {
expect(parse([]).swarm).toBeUndefined();
});

it('rejects prompt mode with --swarm', () => {
const opts = parse(['-p', 'run this', '--swarm']);
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
expect(() => validateOptions(opts)).toThrow('Cannot combine --prompt with --swarm.');
});
});

describe('--auto / --yolo / --plan with --session / --continue', () => {
it('allows --auto with --continue', () => {
const opts = parse(['--auto', '--continue']);
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/run-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function opts(overrides: Partial<Parameters<typeof runPrompt>[0]> = {}) {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: 'say hello',
Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/test/cli/run-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ describe('runShell', () => {
yolo: true,
auto: false,
plan: true,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -265,6 +266,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -305,6 +307,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -343,6 +346,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -381,6 +385,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -414,6 +419,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -465,6 +471,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -503,6 +510,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -533,6 +541,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -570,6 +579,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -623,6 +633,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
swarm: undefined,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
Loading