Skip to content
Merged
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/persistent-experimental-features.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 persistent experimental feature toggles and a TUI panel that applies confirmed changes by reloading the current session.
6 changes: 4 additions & 2 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@moonshot-ai/kimi-code-sdk';

import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app';
import { experimentalFeatureMap } from '#/utils/experimental-features';

import type { CLIOptions, PromptOutputFormat } from './options';
import {
Expand Down Expand Up @@ -146,8 +147,8 @@ export async function runPrompt(
// the turn-run alive across continuation turns, so the normal prompt-turn
// waiter blocks until the goal is terminal; we then emit a summary and set a
// distinct exit code.
const flagMap = await harness.getExperimentalFlags();
const goalCreate = parseHeadlessGoalCreate(opts.prompt!, flagMap['goal-command'] === true);
const flagMap = experimentalFeatureMap(await harness.getExperimentalFeatures());
const goalCreate = parseHeadlessGoalCreate(opts.prompt!, flagMap['goal_command'] === true);
if (goalCreate !== undefined) {
await runHeadlessGoal(session, goalCreate, goalModel, outputFormat, stdout, stderr);
} else {
Expand Down Expand Up @@ -466,6 +467,7 @@ function runPromptTurn(
case 'compaction.completed':
case 'compaction.started':
case 'cron.fired':
case 'goal.updated':
case 'mcp.server.status':
case 'session.meta.updated':
case 'skill.activated':
Expand Down
80 changes: 79 additions & 1 deletion apps/kimi-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import type { PermissionMode, Session } from '@moonshot-ai/kimi-code-sdk';
import type {
ExperimentalFeatureState,
FlagId,
PermissionMode,
Session,
} from '@moonshot-ai/kimi-code-sdk';

import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
import {
ExperimentsSelectorComponent,
type ExperimentalFeatureDraftChange,
} from '../components/dialogs/experiments-selector';
import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model-selector';
import { PermissionSelectorComponent } from '../components/dialogs/permission-selector';
import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector';
Expand All @@ -12,6 +21,7 @@ import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
import { isTheme } from '../theme/index';
import { formatErrorMessage } from '../utils/event-payload';
import { showUsage } from './info';
import { setExperimentalFeatures } from './experimental-flags';
import type { SlashCommandHost } from './dispatch';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -421,6 +431,73 @@ export function showUpdatePreferencePicker(host: SlashCommandHost): void {
);
}

export async function showExperimentsPanel(host: SlashCommandHost): Promise<void> {
let features: readonly ExperimentalFeatureState[];
try {
features = await host.harness.getExperimentalFeatures();
} catch (error) {
host.showError(`Failed to load experimental features: ${formatErrorMessage(error)}`);
return;
}
mountExperimentsPanel(host, features);
}

export async function applyExperimentalFeatureChanges(
host: SlashCommandHost,
changes: readonly ExperimentalFeatureDraftChange[],
): Promise<void> {
if (changes.length === 0) {
host.showStatus(
'No experimental feature changes to apply.',
host.state.theme.colors.textMuted,
);
return;
}

const experimental: Partial<Record<FlagId, boolean>> = {};
for (const change of changes) {
experimental[change.id] = change.enabled;
}

try {
await host.harness.setConfig({ experimental });
Comment thread
liruifengv marked this conversation as resolved.
const features = await host.harness.getExperimentalFeatures();
setExperimentalFeatures(features);
host.refreshSlashCommandAutocomplete();
host.restoreEditor();
if (host.session !== undefined) {
await host.session.reloadSession();
await host.reloadCurrentSessionView(
host.session,
'Experimental features updated. Session reloaded.',
);
} else {
host.showStatus('Experimental features updated.', host.state.theme.colors.success);
}
host.track('experimental_features_apply', { changed: changes.length });
} catch (error) {
host.showError(`Failed to update experimental features: ${formatErrorMessage(error)}`);
}
}

function mountExperimentsPanel(
host: SlashCommandHost,
features: readonly ExperimentalFeatureState[],
): void {
host.mountEditorReplacement(
new ExperimentsSelectorComponent({
features,
colors: host.state.theme.colors,
onApply: (changes) => {
void applyExperimentalFeatureChanges(host, changes);
},
onCancel: () => {
host.restoreEditor();
},
}),
);
}

type UpdatePreferenceHost = {
readonly state: {
readonly appState: Pick<
Expand Down Expand Up @@ -503,6 +580,7 @@ function handleSettingsSelection(host: SlashCommandHost, value: SettingsSelectio
case 'permission': showPermissionPicker(host); return;
case 'theme': showThemePicker(host); return;
case 'editor': showEditorPicker(host); return;
case 'experiments': void showExperimentsPanel(host); return;
case 'upgrade': showUpdatePreferencePicker(host); return;
case 'usage': void showUsage(host); return;
}
Expand Down
13 changes: 13 additions & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
handlePlanCommand,
handleThemeCommand,
handleYoloCommand,
showExperimentsPanel,
showModelPicker,
showPermissionPicker,
showSettingsSelector,
Expand Down Expand Up @@ -68,6 +69,7 @@ export {
handleThemeCommand,
handleYoloCommand,
showModelPicker,
showExperimentsPanel,
showPermissionPicker,
showSettingsSelector,
} from './config';
Expand Down Expand Up @@ -109,6 +111,7 @@ export interface SlashCommandHost {
mountEditorReplacement(panel: Component & Focusable): void;
restoreEditor(): void;
restoreInputText(text: string): void;
refreshSlashCommandAutocomplete(): void;

// Session
requireSession(): Session;
Expand Down Expand Up @@ -171,6 +174,13 @@ async function executeSlashCommand(host: SlashCommandHost, input: string): Promi
host.track('input_command_invalid', { reason: 'blocked', command: intent.commandName });
host.showError(slashBusyMessage(intent.commandName, intent.reason));
return;
case 'invalid':
host.track('input_command_invalid', {
reason: intent.reason,
command: intent.commandName,
});
host.showError(`Invalid slash command: /${intent.commandName}`);
return;
case 'skill': {
const session = host.session;
if (host.state.appState.model.trim().length === 0 || session === undefined) {
Expand Down Expand Up @@ -238,6 +248,9 @@ async function handleBuiltInSlashCommand(
case 'plugins':
void handlePluginsCommand(host, args);
return;
case 'experiments':
await showExperimentsPanel(host);
return;
case 'reload':
await handleReloadCommand(host);
return;
Expand Down
14 changes: 9 additions & 5 deletions apps/kimi-code/src/tui/commands/experimental-flags.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { ExperimentalFlagMap } from '@moonshot-ai/kimi-code-sdk';
import type { ExperimentalFeatureState, ExperimentalFlagMap } from '@moonshot-ai/kimi-code-sdk';

// Resolved experimental flags, fetched once from the core over RPC at startup and then read
import { experimentalFeatureMap } from '#/utils/experimental-features';

// Resolved experimental features, fetched once from the core over RPC at startup and then read
// synchronously by the command palette and dispatch. App-local cache, not a source of truth.
let snapshot: ExperimentalFlagMap = {};

/** Replace the cached flag snapshot. Call once after fetching via `harness.getExperimentalFlags()`. */
export function setExperimentalFlags(flags: ExperimentalFlagMap): void {
snapshot = flags;
/** Replace the cached flag snapshot. Call after fetching via `harness.getExperimentalFeatures()`. */
export function setExperimentalFeatures(
features: readonly Pick<ExperimentalFeatureState, 'id' | 'enabled'>[],
): void {
snapshot = experimentalFeatureMap(features);
}

/** An `undefined` flag means "not gated" → always enabled, so callers can pass an optional flag id. */
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
handlePlanCommand,
handleThemeCommand,
handleYoloCommand,
showExperimentsPanel,
showModelPicker,
showPermissionPicker,
showSettingsSelector,
Expand Down
9 changes: 8 additions & 1 deletion apps/kimi-code/src/tui/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ export const BUILTIN_SLASH_COMMANDS = [
priority: 60,
availability: 'always',
},
{
name: 'experiments',
aliases: ['experimental'],
description: 'Manage experimental features',
priority: 60,
availability: 'idle-only',
},
{
name: 'reload',
aliases: [],
Expand All @@ -139,7 +146,7 @@ export const BUILTIN_SLASH_COMMANDS = [
aliases: [],
description: 'Start or manage an autonomous goal',
priority: 80,
experimentalFlag: 'goal-command',
experimentalFlag: 'goal_command',
// No argumentHint: the menu description stays as short as every other
// command's. The subcommands (status/pause/resume/cancel/replace) surface in
// the argument autocomplete list once the user types `/goal ` (see
Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-code/src/tui/commands/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { KimiConfig } from '@moonshot-ai/kimi-code-sdk';

import { loadTuiConfig, type TuiConfig } from '../config';
import type { SlashCommandHost } from './dispatch';
import { setExperimentalFeatures } from './experimental-flags';

export async function handleReloadTuiCommand(host: SlashCommandHost): Promise<void> {
const tuiConfig = await loadTuiConfig();
Expand All @@ -19,6 +20,8 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise<void>
}

const config = await host.harness.getConfig({ reload: true });
setExperimentalFeatures(await host.harness.getExperimentalFeatures());
host.refreshSlashCommandAutocomplete();
applyRuntimeConfig(host, config);
applyReloadedTuiConfig(host, tuiConfig);

Expand Down
Loading
Loading