Skip to content

Commit 98d0ae5

Browse files
committed
feat: configurable default swarm mode
1 parent f874251 commit 98d0ae5

25 files changed

Lines changed: 367 additions & 10 deletions

.changeset/default-swarm-mode.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@moonshot-ai/agent-core": minor
3+
"@moonshot-ai/kimi-code-sdk": minor
4+
"@moonshot-ai/kimi-code": minor
5+
---
6+
7+
Add configurable default swarm mode via `default_swarm_mode` in config.toml and `--swarm` / `--no-swarm` CLI flags.

apps/kimi-code/src/cli/commands.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export function createProgram(
7373
)
7474
.addOption(new Option('--yes').hideHelp().default(false))
7575
.addOption(new Option('--auto-approve').hideHelp().default(false))
76-
.option('--plan', 'Start in plan mode.', false);
76+
.option('--plan', 'Start in plan mode.', false)
77+
.option('--swarm', 'Start in swarm mode.')
78+
.option('--no-swarm', 'Do not start in swarm mode.');
7779

7880
registerExportCommand(program);
7981
registerProviderCommand(program);
@@ -115,6 +117,7 @@ export function createProgram(
115117
yolo: yoloValue,
116118
auto: autoValue,
117119
plan: raw['plan'] as boolean,
120+
swarm: raw['swarm'] as boolean | undefined,
118121
model: raw['model'] as string | undefined,
119122
outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'],
120123
prompt: raw['prompt'] as string | undefined,

apps/kimi-code/src/cli/options.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface CLIOptions {
77
yolo: boolean;
88
auto: boolean;
99
plan: boolean;
10+
swarm: boolean | undefined;
1011
model: string | undefined;
1112
outputFormat: PromptOutputFormat | undefined;
1213
prompt: string | undefined;
@@ -46,6 +47,9 @@ export function validateOptions(opts: CLIOptions): ValidatedOptions {
4647
if (promptMode && opts.plan) {
4748
throw new OptionConflictError('Cannot combine --prompt with --plan.');
4849
}
50+
if (promptMode && opts.swarm) {
51+
throw new OptionConflictError('Cannot combine --prompt with --swarm.');
52+
}
4953
if (promptMode && opts.session === '') {
5054
throw new OptionConflictError('Cannot use --session without an id in prompt mode.');
5155
}

apps/kimi-code/src/cli/run-shell.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export async function runShell(
104104
startupNotice: configWarning,
105105
migrationPlan,
106106
migrateOnly: runOptions.migrateOnly,
107+
defaultSwarmMode: config.defaultSwarmMode,
107108
});
108109

109110
initializeCliTelemetry({

apps/kimi-code/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = {
109109
yolo: false,
110110
auto: false,
111111
plan: false,
112+
swarm: undefined,
112113
model: undefined,
113114
outputFormat: undefined,
114115
prompt: undefined,

apps/kimi-code/src/tui/kimi-tui.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { CompactionComponent } from './components/dialogs/compaction';
5757
import { HelpPanelComponent } from './components/dialogs/help-panel';
5858
import { QuestionDialogComponent } from './components/dialogs/question-dialog';
5959
import { SessionPickerComponent } from './components/dialogs/session-picker';
60+
import { SwarmStartPermissionPromptComponent } from './components/dialogs/swarm-start-permission-prompt';
6061
import {
6162
FileMentionProvider,
6263
type SlashAutocompleteCommand,
@@ -70,6 +71,7 @@ import {
7071
GoalSetMessageComponent,
7172
} from './components/messages/goal-panel';
7273
import { SkillActivationComponent } from './components/messages/skill-activation';
74+
import { SwarmModeMarkerComponent } from './components/messages/swarm-markers';
7375
import {
7476
NoticeMessageComponent,
7577
StatusMessageComponent,
@@ -150,6 +152,8 @@ export interface KimiTUIStartupInput {
150152
readonly migrationPlan?: MigrationPlan | null;
151153
/** When true, run only the migration screen, then exit (the `kimi migrate` command). */
152154
readonly migrateOnly?: boolean;
155+
/** Default swarm mode from config.toml; CLI flags override this. */
156+
readonly defaultSwarmMode?: boolean;
153157
}
154158

155159
type EffectiveActivityPaneMode = ActivityPaneMode | 'idle' | 'session';
@@ -160,13 +164,14 @@ function createInitialAppState(input: KimiTUIStartupInput): AppState {
160164
: input.cliOptions.yolo
161165
? 'yolo'
162166
: 'manual';
167+
const startupSwarm = input.cliOptions.swarm ?? input.defaultSwarmMode ?? false;
163168
return {
164169
model: '',
165170
workDir: input.workDir,
166171
sessionId: '',
167172
permissionMode: startupPermission,
168173
planMode: input.cliOptions.plan,
169-
swarmMode: false,
174+
swarmMode: startupSwarm,
170175
thinking: false,
171176
contextUsage: 0,
172177
contextTokens: 0,
@@ -260,6 +265,7 @@ export class KimiTUI {
260265
yolo: startupInput.cliOptions.yolo,
261266
auto: startupInput.cliOptions.auto,
262267
plan: startupInput.cliOptions.plan,
268+
swarm: startupInput.cliOptions.swarm,
263269
model: startupInput.cliOptions.model,
264270
startupNotice: startupInput.startupNotice,
265271
},
@@ -514,6 +520,58 @@ export class KimiTUI {
514520
this.updateTerminalTitle();
515521
}
516522
void this.refreshSkillCommands(this.session);
523+
if (!shouldReplayHistory) {
524+
void this.promptForSwarmPermissionIfNeeded();
525+
}
526+
}
527+
528+
private async promptForSwarmPermissionIfNeeded(): Promise<void> {
529+
if (!this.state.appState.swarmMode || this.state.appState.permissionMode !== 'manual') {
530+
return;
531+
}
532+
const session = this.session;
533+
if (session === undefined) return;
534+
535+
this.deferUserMessages = true;
536+
const restore = (): void => {
537+
this.deferUserMessages = false;
538+
this.restoreEditor();
539+
};
540+
541+
this.mountEditorReplacement(
542+
new SwarmStartPermissionPromptComponent({
543+
onSelect: (choice) => {
544+
restore();
545+
if (choice === 'auto' || choice === 'yolo') {
546+
void (async () => {
547+
try {
548+
await session.setPermission(choice);
549+
} catch (error) {
550+
this.showError(`Failed to set permission mode: ${formatErrorMessage(error)}`);
551+
await this.disableStartupSwarmMode(session);
552+
return;
553+
}
554+
this.setAppState({ permissionMode: choice });
555+
})();
556+
}
557+
},
558+
onCancel: () => {
559+
restore();
560+
void this.disableStartupSwarmMode(session);
561+
},
562+
}),
563+
);
564+
}
565+
566+
private async disableStartupSwarmMode(session: Session): Promise<void> {
567+
try {
568+
await session.setSwarmMode(false, 'manual');
569+
} catch (error) {
570+
this.showError(`Failed to disable swarm mode: ${formatErrorMessage(error)}`);
571+
}
572+
this.setAppState({ swarmMode: false });
573+
this.state.transcriptContainer.addChild(new SwarmModeMarkerComponent('inactive'));
574+
this.state.ui.requestRender();
517575
}
518576

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

542601
try {
@@ -1090,10 +1149,10 @@ export class KimiTUI {
10901149
});
10911150
}
10921151

1093-
// Apply --auto/--yolo/--plan startup flags to a resumed session. The resumed
1094-
// session may already be in plan mode from its persisted records, and
1095-
// re-entering plan mode throws, so only enable it when it is not active yet.
1096-
// setPermission is idempotent and needs no such guard.
1152+
// Apply --auto/--yolo/--plan/--swarm startup flags to a resumed session. The
1153+
// resumed session may already be in plan/swarm mode from its persisted
1154+
// records, and re-entering plan mode throws, so only enable it when it is not
1155+
// active yet. setPermission is idempotent and needs no such guard.
10971156
private async applyStartupModesToResumedSession(session: Session): Promise<void> {
10981157
const { startup } = this.options;
10991158
if (startup.auto) {
@@ -1107,6 +1166,12 @@ export class KimiTUI {
11071166
await session.setPlanMode(true);
11081167
}
11091168
}
1169+
if (startup.swarm) {
1170+
const status = await session.getStatus();
1171+
if (!status.swarmMode) {
1172+
await session.setSwarmMode(true, 'manual');
1173+
}
1174+
}
11101175
}
11111176

11121177
// Re-apply startup flags that the user explicitly passed on the command line.

apps/kimi-code/src/tui/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export interface TUIStartupOptions {
192192
readonly yolo: boolean;
193193
readonly auto: boolean;
194194
readonly plan: boolean;
195+
readonly swarm?: boolean;
195196
readonly model?: string;
196197
readonly startupNotice?: string;
197198
}

apps/kimi-code/test/cli/main.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ function defaultOpts(): CLIOptions {
140140
yolo: false,
141141
auto: false,
142142
plan: false,
143+
swarm: undefined,
143144
model: undefined,
144145
outputFormat: undefined,
145146
prompt: undefined,

apps/kimi-code/test/cli/options.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,26 @@ describe('CLI options parsing', () => {
167167
});
168168
});
169169

170+
describe('--swarm / --no-swarm', () => {
171+
it('sets swarm mode flag with --swarm', () => {
172+
expect(parse(['--swarm']).swarm).toBe(true);
173+
});
174+
175+
it('clears swarm mode flag with --no-swarm', () => {
176+
expect(parse(['--no-swarm']).swarm).toBe(false);
177+
});
178+
179+
it('leaves swarm mode unspecified when the flag is absent', () => {
180+
expect(parse([]).swarm).toBeUndefined();
181+
});
182+
183+
it('rejects prompt mode with --swarm', () => {
184+
const opts = parse(['-p', 'run this', '--swarm']);
185+
expect(() => validateOptions(opts)).toThrow(OptionConflictError);
186+
expect(() => validateOptions(opts)).toThrow('Cannot combine --prompt with --swarm.');
187+
});
188+
});
189+
170190
describe('--auto / --yolo / --plan with --session / --continue', () => {
171191
it('allows --auto with --continue', () => {
172192
const opts = parse(['--auto', '--continue']);

apps/kimi-code/test/cli/run-prompt.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ function opts(overrides: Partial<Parameters<typeof runPrompt>[0]> = {}) {
131131
yolo: false,
132132
auto: false,
133133
plan: false,
134+
swarm: undefined,
134135
model: undefined,
135136
outputFormat: undefined,
136137
prompt: 'say hello',

0 commit comments

Comments
 (0)