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
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