Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions .changeset/plugin-reload-hot-apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code-sdk": minor
"@moonshot-ai/kimi-code": minor
---

`/plugins reload` now hot-applies plugin changes to the current session — no `/new` required. Newly installed or enabled plugin skills load immediately (the main agent's skill list and `Skill` tool are refreshed) and newly enabled plugin MCP servers are connected. Disable, remove, update, and `sessionStart` changes are not torn down in a running session; reload reports when a new session is still needed to fully apply them.

Adds `PluginManager.runtimeSnapshot()` and `Session.applyPluginRuntimeSnapshot()` in `agent-core`; the SDK's `reloadPlugins()` now returns the applied result (`PluginReloadResult` / `PluginRuntimeApplyResult`).
2 changes: 2 additions & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from './config';
import { handleFeedbackCommand, showMcpServers, showStatusReport, showUsage } from './info';
import { handlePluginsCommand } from './plugins';
import type { SkillListSession } from './skills';
import {
handleExportDebugZipCommand,
handleExportMdCommand,
Expand Down Expand Up @@ -121,6 +122,7 @@ export interface SlashCommandHost {
showSessionPicker(): Promise<void>;
sendNormalUserInput(text: string): void;
sendSkillActivation(session: Session, skillName: string, skillArgs: string): void;
refreshSkillCommands(session?: SkillListSession): Promise<void>;
readonly skillCommandMap: Map<string, string>;

// Controller refs
Expand Down
47 changes: 39 additions & 8 deletions apps/kimi-code/src/tui/commands/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { homedir as osHomedir } from 'node:os';
import { isAbsolute, join, resolve } from 'node:path';

import chalk from 'chalk';

import type { PluginInfo, PluginSummary } from '@moonshot-ai/kimi-code-sdk';

import {
Expand Down Expand Up @@ -87,7 +89,7 @@ export async function handlePluginsCommand(host: SlashCommandHost, rawArgs: stri
}
await session.setPluginMcpServerEnabled(id, server, action === 'enable');
host.showStatus(
`${action === 'enable' ? 'Enabled' : 'Disabled'} MCP server ${server} for ${id}. Run /new to apply.`,
`${action === 'enable' ? 'Enabled' : 'Disabled'} MCP server ${server} for ${id}. Run ${reloadCommandText(host.state.theme.colors)} to apply to this session.`,
);
return;
}
Expand Down Expand Up @@ -264,7 +266,9 @@ async function applyPluginEnabled(
? ` Some MCP servers are disabled; re-enable with /plugins mcp enable ${id} <server>.`
: '';
if (showStatus) {
host.showStatus(`${enabled ? 'Enabled' : 'Disabled'} ${id}. Run /new to apply.${mcpHint}`);
host.showStatus(
`${enabled ? 'Enabled' : 'Disabled'} ${id}. Run ${reloadCommandText(host.state.theme.colors)} to apply to this session.${mcpHint}`,
);
}
const inlineMcpHint = mcpHint.length > 0 ? ' · MCP servers disabled' : '';
return `${pluginInlineChangeHint()}${inlineMcpHint}`;
Expand Down Expand Up @@ -393,22 +397,39 @@ async function installPluginFromSource(
? ` Declares ${summary.mcpServerCount} MCP ${serverWord}; enabled by default and configurable from /plugins.`
: '';
const installVerb = options?.successNotice === 'marketplace' ? 'Installed or updated' : 'Installed';
const reloadCmd = reloadCommandText(host.state.theme.colors);
host.showStatus(
`${installVerb} ${summary.displayName} (${summary.id}).${mcpHint} Run /new to apply plugin changes.`,
`${installVerb} ${summary.displayName} (${summary.id}).${mcpHint} Run ${reloadCmd} to apply to this session.`,
);
if (options?.successNotice === 'marketplace') {
host.showNotice(
`Installed or updated ${summary.displayName}`,
`Marketplace install or update succeeded for ${summary.id}. Run /new to apply plugin changes.`,
`Marketplace install or update succeeded for ${summary.id}. Run ${reloadCmd} to apply to this session.`,
);
}
}

async function reloadPlugins(host: SlashCommandHost): Promise<void> {
const summary = await host.requireSession().reloadPlugins();
const line = `Reload: +${summary.added.length} -${summary.removed.length}` +
(summary.errors.length > 0 ? ` (${summary.errors.length} errors)` : '');
const session = host.requireSession();
const summary = await session.reloadPlugins();
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 Keep hot reload out of active turns

/plugins is registered as available while streaming, and slash commands bypass the normal message queue, so running /plugins reload during an active response now mutates the live session immediately by loading skills, rerendering the main system prompt, and connecting MCP servers. The commit describes reload as a safe apply point between turns, but without an idle guard here the next tool-loop/model call in the same turn can see a different prompt/tool set than the one the turn started with; block the reload subcommand while streamingPhase/compaction is active or queue it until idle.

Useful? React with 👍 / 👎.

const applied = summary.applied;
const parts = [`+${summary.added.length} plugins`];
if (applied !== undefined) {
parts.push(
`${applied.addedSkills.length} skills`,
`${applied.addedMcpServers.length} MCP servers`,
);
}
let line = `Reload: ${parts.join(', ')} now active`;
if (summary.errors.length > 0) {
line += ` (${summary.errors.length} errors)`;
}
if (summary.removed.length > 0 || (applied?.needsNewSession ?? false)) {
line += '. Removals, updates, and sessionStart changes need a new session to fully apply.';
}
host.showStatus(line);
// New skills may add slash commands; refresh the palette/autocomplete.
await host.refreshSkillCommands(session);
}

function resolvePluginInstallSource(source: string, workDir: string): string {
Expand All @@ -420,5 +441,15 @@ function resolvePluginInstallSource(source: string, workDir: string): string {
}

function pluginInlineChangeHint(): string {
return 'pending /new';
return 'pending reload';
}

/**
* Render a `/plugins reload` reference with the theme accent + bold so the
* call to action stands out against the dimmed status line. chalk restores the
* surrounding status color after the highlighted span, so it composes with the
* status component's own coloring.
*/
function reloadCommandText(colors: { readonly accent: string }): string {
return chalk.hex(colors.accent).bold('/plugins reload');
}
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,15 @@ describe('plugins selector dialogs', () => {
},
],
selectedId: 'kimi-datasource',
pluginHint: { id: 'kimi-datasource', text: 'pending /new' },
pluginHint: { id: 'kimi-datasource', text: 'pending reload' },
colors: darkColors,
onSelect: vi.fn(),
onCancel: vi.fn(),
});

const out = picker.render(120).map(strip).join('\n');

expect(out).toContain('? Kimi Datasource enabled pending /new');
expect(out).toContain('? Kimi Datasource enabled pending reload');
});

it('defaults plugin removal confirmation to cancel', () => {
Expand Down
41 changes: 37 additions & 4 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,9 +1486,11 @@ describe('KimiTUI message flow', () => {
);
});
const out = stripSgr(driver.state.editorContainer.children[0]!.render(120).join('\n'));
expect(out).toContain('❯ Demo disabled pending /new');
expect(out).toContain('❯ Demo disabled pending reload');
expect(out).not.toContain('Space enable');
expect(stripSgr(renderTranscript(driver))).not.toContain('Disabled demo. Run /new to apply.');
expect(stripSgr(renderTranscript(driver))).not.toContain(
'Disabled demo. Run /plugins reload to apply to this session.',
);
});

it('toggles plugin MCP servers from the overview MCP picker', async () => {
Expand Down Expand Up @@ -1579,10 +1581,41 @@ describe('KimiTUI message flow', () => {
expect(driver.state.editorContainer.children[0]).toBeInstanceOf(PluginMcpSelectorComponent);
});
const out = stripSgr(driver.state.editorContainer.children[0]!.render(120).join('\n'));
expect(out).toContain('❯ data disabled pending /new');
expect(out).toContain('❯ data disabled pending reload');
expect(stripSgr(renderTranscript(driver))).not.toContain(
'Disabled MCP server data for kimi-datasource. Run /new to apply.',
'Disabled MCP server data for kimi-datasource. Run /plugins reload to apply to this session.',
);
});

it('reports the reload summary and refreshes skill commands on /plugins reload', async () => {
const session = makeSession({
reloadPlugins: vi.fn(async () => ({
added: [],
removed: [],
errors: [],
applied: {
addedSkills: ['hot-skill'],
addedMcpServers: ['plugin-demo:data'],
needsNewSession: true,
},
})),
});
const { driver } = await makeDriver(session);
const refreshSpy = vi.spyOn(
driver as unknown as { refreshSkillCommands: (...args: unknown[]) => Promise<void> },
'refreshSkillCommands',
);

driver.handleUserInput('/plugins reload');

// The status line wraps at the terminal width, so collapse whitespace.
const transcript = () => stripSgr(renderTranscript(driver)).replaceAll(/\s+/g, ' ');
await vi.waitFor(() => {
expect(transcript()).toContain('Reload: +0 plugins, 1 skills, 1 MCP servers now active');
});
expect(transcript()).toContain('need a new session to fully apply');
expect(session.reloadPlugins).toHaveBeenCalled();
expect(refreshSpy).toHaveBeenCalled();
});

it('requires confirmation before /plugins remove removes a plugin', async () => {
Expand Down
2 changes: 1 addition & 1 deletion docs/en/customization/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Project entries override user-level entries with the same name.

The easiest entry point is running `/mcp-config` in the TUI, which guides you through adding, editing, or removing servers. To check connection status, run `/mcp`.

Plugins can also declare MCP servers in `kimi.plugin.json` or `.kimi-plugin/plugin.json`. Plugin-declared servers are enabled by default but only start in new sessions; disable or re-enable them from `/plugins` or with `/plugins mcp disable|enable <plugin-id> <server>`, then start a new session. See [Plugins](./plugins.md) for details.
Plugins can also declare MCP servers in `kimi.plugin.json` or `.kimi-plugin/plugin.json`. Plugin-declared servers are enabled by default; a newly installed or enabled one comes online in the current session after `/plugins reload` (or in a new session). Disable or re-enable them from `/plugins` or with `/plugins mcp disable|enable <plugin-id> <server>` — enabling applies on `/plugins reload`, while disabling needs a new session to fully take effect. See [Plugins](./plugins.md) for details.

The top-level shape of `mcp.json` is:

Expand Down
14 changes: 8 additions & 6 deletions docs/en/customization/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ Most users only need the interactive manager. You can also use these slash comma
| `/plugins enable <id>` | Enable a plugin; opens the manager when `<id>` is omitted. |
| `/plugins disable <id>` | Disable a plugin; opens the manager when `<id>` is omitted. |
| `/plugins remove <id>` | Remove a plugin; requires confirmation. |
| `/plugins reload` | Reload `installed.json` and each plugin manifest. |
| `/plugins reload` | Reload `installed.json` and each plugin manifest, and hot-apply newly added skills and newly enabled MCP servers to the current session. |
| `/plugins mcp enable <id> <server>` | Enable an MCP server declared by a plugin. |
| `/plugins mcp disable <id> <server>` | Disable an MCP server declared by a plugin. |

For general slash command behavior, see [Slash commands](../reference/slash-commands.md).

Kimi Code CLI currently installs plugins per user. Records are stored under `$KIMI_CODE_HOME/plugins/` and apply across all projects. Project-local, repository-shared, admin-managed, and `--scope` installs are not supported yet.

Plugin changes apply to new sessions only. After installing, enabling, disabling, removing, or reloading a plugin, or changing an MCP server toggle, start a fresh session with `/new`. The current session is not updated; new skills, session-start behavior, and MCP servers load only in new sessions.
After installing or enabling a plugin (or enabling one of its MCP servers), run `/plugins reload` to apply the change to the current session — no `/new` required. Reload hot-loads newly added skills (the main agent's skill list and the `Skill` tool are refreshed) and connects newly enabled MCP servers; their tools become available on the next turn. Additive changes only: disabling or removing a plugin, updating one, and `sessionStart` injections are not torn down in a running session. When any of those are pending, `/plugins reload` reports that a new session (`/new`) is still required to fully apply them.

Local installs are copied into `$KIMI_CODE_HOME/plugins/managed/<id>/`, and Kimi Code CLI always runs from that managed copy. Editing the original source directory after install has no effect until you reinstall — `/plugins reload` re-reads install records and manifests, not the original source. Removing a plugin deletes only its install record; the managed copy and the original source files are left on disk.
Local installs are copied into `$KIMI_CODE_HOME/plugins/managed/<id>/`, and Kimi Code CLI always runs from that managed copy. Editing the original source directory after install has no effect until you reinstall — `/plugins reload` re-reads install records and manifests (not the original source) and applies the additive changes above. Removing a plugin deletes only its install record; the managed copy and the original source files are left on disk.

## Plugin manifest

Expand Down Expand Up @@ -134,14 +134,16 @@ HTTP server:

For stdio servers, `command` may be a command on `PATH` or a `./` path inside the plugin root. If `cwd` is set, it must also start with `./` and stay inside the plugin root; other values are rejected and the server is omitted. Plugin MCP servers inherit the current process environment; values under `env` are literal overrides.

Plugin MCP servers start only in new sessions. To disable or re-enable one, run `/plugins`, select the plugin, and press `M`. Shortcut commands are also available:
Newly enabled plugin MCP servers come online when you run `/plugins reload` (or in a new session). To disable or re-enable one, run `/plugins`, select the plugin, and press `M`. Shortcut commands are also available:

```sh
# Disabling is not torn down live — a new session fully applies it:
/plugins mcp disable kimi-finance finance
/new

# Enabling takes effect in the current session after a reload:
/plugins mcp enable kimi-finance finance
/new
/plugins reload
```

## Security model
Expand All @@ -151,5 +153,5 @@ Plugins expose a limited loading surface:
- Install and session startup read only plugin manifests and Markdown skill files.
- All paths must stay inside the plugin root after symlinks are resolved.
- Command-backed plugin tools, hooks, and legacy tool runtimes are not executed by the plugin loader.
- MCP servers declared by enabled plugins start only in new sessions and can be disabled from `/plugins`.
- MCP servers declared by enabled plugins come online via `/plugins reload` (or a new session) and can be disabled from `/plugins`.
- Bad manifests or unsafe paths produce diagnostics in `/plugins info <id>` without crashing unrelated sessions.
2 changes: 1 addition & 1 deletion docs/en/reference/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Some commands are only available in the idle state. Running them while the sessi
| `/usage` | — | Show token usage, context consumption, and quota information. | Yes |
| `/status` | — | Show the current session runtime status, including version, model, working directory, and permission mode. | Yes |
| `/mcp` | — | List the MCP servers in the current session and their connection status. | Yes |
| `/plugins` | — | Open the interactive plugin manager for user/global installs: install, inspect, enable, disable, confirm removal, reload, browse the official marketplace, and toggle plugin MCP servers. Shortcut subcommands remain available. | Yes |
| `/plugins` | — | Open the interactive plugin manager for user/global installs: install, inspect, enable, disable, confirm removal, reload (hot-applies new skills and MCP servers to the current session), browse the official marketplace, and toggle plugin MCP servers. Shortcut subcommands remain available. | Yes |
| `/version` | — | Show the Kimi Code CLI version number. | Yes |
| `/feedback` | — | Submit feedback to help improve Kimi Code CLI. | Yes |

Expand Down
2 changes: 1 addition & 1 deletion docs/zh/customization/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ MCP server 配置写在 `mcp.json` 中,分为两层:

最方便的入口是在 TUI 中运行 `/mcp-config`,它会引导你新增、编辑或删除 server。要查看当前连接状态,可运行 `/mcp`。

Plugins 也可以在 `kimi.plugin.json` 或 `.kimi-plugin/plugin.json` 中声明 MCP servers。Plugin 声明的 servers 默认启用,但只会在新会话中启动;可以在 `/plugins` 中禁用或重新启用,也可以使用 `/plugins mcp disable|enable <plugin-id> <server>`,然后开启新会话。详见 [Plugins](./plugins.md)。
Plugins 也可以在 `kimi.plugin.json` 或 `.kimi-plugin/plugin.json` 中声明 MCP servers。Plugin 声明的 servers 默认启用;新安装或新启用的 server 在运行 `/plugins reload`(或新会话)后即在当前会话上线。可以在 `/plugins` 中禁用或重新启用,也可以使用 `/plugins mcp disable|enable <plugin-id> <server>`——启用在 `/plugins reload` 后生效,禁用则需开启新会话才能完全生效。详见 [Plugins](./plugins.md)。

`mcp.json` 的顶层结构如下:

Expand Down
Loading
Loading