From 5f7bc2d0e831351c203fd9b89edc94393af79bdf Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 2 Jun 2026 23:00:27 +0800 Subject: [PATCH 1/4] Create CONFIG_RELOAD_RESEARCH.md --- CONFIG_RELOAD_RESEARCH.md | 339 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 CONFIG_RELOAD_RESEARCH.md diff --git a/CONFIG_RELOAD_RESEARCH.md b/CONFIG_RELOAD_RESEARCH.md new file mode 100644 index 0000000000..6572d84109 --- /dev/null +++ b/CONFIG_RELOAD_RESEARCH.md @@ -0,0 +1,339 @@ +# /config-reload 调研文档 + +## 背景 + +目标是新增一个 TUI 斜杠命令 `/config-reload`,用于用户手动修改配置文件后,在不重启 kimi-code 的情况下重新加载并应用配置。这里的“配置”至少涉及两类: + +- Core 运行时配置:`~/.kimi-code/config.toml`,由 `packages/agent-core/src/config/*` 解析,影响 provider/model、thinking、permission、hooks、skills、runtime services、background 等。 +- TUI 客户端偏好:`~/.kimi-code/tui.toml`,由 `apps/kimi-code/src/tui/config.ts` 解析,影响 theme、editor、notifications、upgrade。 + +本次只做代码调研和实现建议,不写 TypeScript 实现。 + +## 当前配置加载链路 + +### Core config.toml + +`KimiCore` 构造时解析配置路径并读取一次运行时配置: + +- `packages/agent-core/src/rpc/core-impl.ts:139-159`:解析 `homeDir/configPath`,`this.config = loadRuntimeConfig(this.configPath)`。 +- `packages/agent-core/src/config/toml.ts`:`readConfigFile()` 读取 TOML;`loadRuntimeConfig()` 在磁盘配置上叠加 `KIMI_MODEL_*` 环境变量合成的 provider/model。 +- `packages/agent-core/src/config/schema.ts`:`KimiConfigSchema` 当前覆盖 provider/model/thinking/permission/hooks/services/skills/loop/background/telemetry 等字段。 + +已有的全局配置 RPC: + +- `packages/agent-core/src/rpc/core-impl.ts:382-386`:`getKimiConfig({ reload: true })` 会重新执行 `loadRuntimeConfig()` 并替换 `KimiCore.this.config`。 +- `packages/agent-core/src/rpc/core-impl.ts:389-393`:`setKimiConfig()` 写回文件后也会刷新 `this.config`。 +- `packages/agent-core/src/rpc/core-impl.ts:442-448`:`setModel()` 前会调用 `reloadProviderManager()`,因此同一 session 切换模型时能看到外部编辑后的 provider/model 配置。 + +### Session 创建/恢复时的配置消费 + +创建 session 时会重新读取 Core 配置,并把配置拆开传给 `Session`: + +- `packages/agent-core/src/rpc/core-impl.ts:172-216`:`createSession()` 调用 `reloadProviderManager()`,然后把 `config`、`background`、`hooks`、`permissionRules`、`skills`、`mcpConfig`、`toolServices` 等传进 `Session`。 +- `packages/agent-core/src/rpc/core-impl.ts:230-240`:新 main agent 初始化 `modelAlias/thinkingLevel/defaultPlanMode/permissionMode`。 +- `packages/agent-core/src/rpc/core-impl.ts:711-735`:恢复 session 后会尝试用当前 config 重新校验/刷新模型 alias,但这是 resume 专用逻辑,不是通用热重载入口。 + +`ProviderManager` 是当前最接近热重载的部分: + +- `packages/agent-core/src/rpc/core-impl.ts:677-684`:`ProviderManager` 拿到的是 `config: () => this.config`。 +- 因此当 `KimiCore.this.config` 更新后,已有 Agent 的 provider/model 解析可以看到新配置,但前提是有触发点,例如 `setModel()` 或下一次构造 provider。 + +### Agent/Session 中固定下来的配置 + +大量配置在 `Session` 或 `Agent` 构造时固定,不会因为 `KimiCore.this.config` 被替换而自动更新: + +- `packages/agent-core/src/session/index.ts:112-149`:`Session` 构造时创建 `HookEngine`、`SkillRegistry`、`McpConnectionManager`,并异步 `loadSkills()` / `loadMcpServers()`。 +- `packages/agent-core/src/session/index.ts:325-339`:skills roots 由构造时 `options.skills` 决定。 +- `packages/agent-core/src/session/index.ts:341-364`:MCP server 由构造时 `options.mcpConfig` 决定,只初始 `connectAll()`。 +- `packages/agent-core/src/session/index.ts:413-432`:每个 Agent 构造时拿到 `config: this.options.config`、`toolServices`、`hookEngine`、`mcp`、`permission` 等。 +- `packages/agent-core/src/agent/index.ts:128-141`:`Agent.kimiConfig`、`toolServices`、`mcp`、`hooks` 都是 readonly 引用。 +- `packages/agent-core/src/agent/config/index.ts:51-55`:thinking level 解析依赖 `agent.kimiConfig?.thinking`,已有 Agent 持有的是旧 `KimiConfig` 对象。 +- `packages/agent-core/src/agent/permission/index.ts:37-55`:permission rules 初始化后保存在 `PermissionManager.rules`,当前无公开替换方法。 +- `packages/agent-core/src/rpc/core-impl.ts:655-663`:`resolveRuntime()` 会缓存 `this.runtime`,所以 `services.moonshotSearch/moonshotFetch` 改动不会自动重建 WebSearch/FetchURL provider。 + +### MCP 和插件 + +MCP 配置不是 `config.toml`,而是: + +- `~/.kimi-code/mcp.json` +- `/.kimi-code/mcp.json` + +相关代码: + +- `packages/agent-core/src/mcp/config-loader.ts`:加载 user/project 两处 `mcp.json`,project 覆盖 user。 +- `packages/agent-core/src/mcp/session-config.ts`:`resolveSessionMcpConfig()` 包装成 session 配置。 +- `packages/agent-core/src/rpc/core-impl.ts:686-694`:插件 MCP server 会合并进 session MCP config。 + +当前 MCP 连接管理器的能力: + +- `packages/agent-core/src/mcp/connection-manager.ts:145-186`:`connectAll()` 会把传入配置写进 entries 并连接,但不是安全的 reload API;它不会处理已删除 server,也不会先关闭同名旧 client。 +- `packages/agent-core/src/mcp/connection-manager.ts:189-205`:`reconnect(name)` 只重连已有 server。 +- `packages/agent-core/src/mcp/connection-manager.ts:208-213`:`shutdown()` 可以关闭全部并清空。 + +插件 reload 已存在: + +- `packages/agent-core/src/rpc/core-impl.ts:616-629`:`reloadPlugins()` 只刷新 `PluginManager` 中的插件记录。 +- `packages/agent-core/src/plugin/manager.ts:180-199`:`PluginManager.reload()` 返回 added/removed/errors。 +- 但已存在 session 的 `SkillRegistry`、plugin MCP server、plugin sessionStart 不会因为 `/plugins reload` 自动更新。 + +## TUI 侧链路 + +### Slash 命令注册和分发 + +新增 slash 命令的热路径: + +- `apps/kimi-code/src/tui/commands/registry.ts`:注册命令名、描述、优先级、availability。 +- `apps/kimi-code/src/tui/commands/dispatch.ts`:`handleBuiltInSlashCommand()` 分发到具体 handler。 +- `apps/kimi-code/src/tui/commands/config.ts`:现有 model/theme/editor/permission/settings 等配置类命令集中在这里。 + +### TUI 状态同步 + +启动时: + +- `apps/kimi-code/src/tui/kimi-tui.ts:470-536`:`init()` 先 `authFlow.refreshAvailableModels()`,再 create/resume session。 +- `apps/kimi-code/src/tui/controllers/auth-flow.ts:29-36`:`refreshAvailableModels()` 调用 `harness.getConfig({ reload: true })`,并更新 `availableModels/availableProviders`。 + +运行时状态: + +- `apps/kimi-code/src/tui/kimi-tui.ts:1004-1018`:`syncRuntimeState()` 从 `session.getStatus()` 同步当前 model/thinking/permission/plan/context。 +- `apps/kimi-code/src/tui/kimi-tui.ts:1531-1541`:`applyTheme()` 可立即应用 theme 到 TUI。 +- `apps/kimi-code/src/tui/commands/config.ts:334-344`:`persistModelSelection()` 已会 `getConfig({ reload: true })` 后再写默认模型。 + +TUI 偏好配置: + +- `apps/kimi-code/src/tui/config.ts`:`loadTuiConfig()` / `saveTuiConfig()` 管理 `tui.toml`。 +- `apps/kimi-code/src/cli/run-shell.ts:33-47`:`tui.toml` 只在 shell 启动时读取一次,`theme = "auto"` 也只在启动时解析一次初始 terminal theme。 +- 已有 `/theme`、`/editor`、settings 命令会保存并即时更新局部 TUI state,但没有“重新读取 tui.toml 并应用”的命令。 + +## 已有能力可以直接复用的部分 + +1. 重读 Core config 文件: + - `harness.getConfig({ reload: true })` 已可触发 Core 重新读取 `config.toml`。 + +2. 刷新 TUI 的可选模型列表: + - `authFlow.refreshAvailableModels()` 已封装读取配置并更新 `availableModels/availableProviders`。 + +3. 应用当前 session 的 model/thinking/permission: + - `session.setModel(alias)` 会触发 Core 先刷新配置再解析新 alias。 + - `session.setThinking(level)` / `session.setPermission(mode)` 可更新已有 Agent 的运行态。 + - `syncRuntimeState()` 可重新拉取并同步 footer/status 需要的 session 状态。 + +4. 应用 TUI theme: + - `loadTuiConfig()` 可重新读取 `tui.toml`。 + - `applyTheme()` 可即时更新 UI theme。 + - editor/notifications/upgrade 可以直接 `setAppState()`。 + +## 关键缺口 + +### 缺口 1:没有 Core 级 `reloadConfig` 语义 + +`getKimiConfig({ reload: true })` 只保证 Core 内存配置更新,不表达“应用到当前 session/agent”。如果 TUI 自己拼装 reload 行为,会导致 SDK/API 语义不清,其他客户端也无法复用。 + +建议新增 Core RPC: + +- `reloadKimiConfig(payload): ReloadConfigResult` +- SDK 暴露 `KimiHarness.reloadConfig()` 或 `Session.reloadConfig()`,具体取决于是否允许一次刷新所有 active sessions。 + +### 缺口 2:已有 Agent 持有旧 `KimiConfig` + +已有 Agent 的 `agent.kimiConfig` 是 readonly,thinking 默认值、loopControl 等都基于旧对象。只替换 `KimiCore.this.config` 不足以更新这些状态。 + +可选方向: + +- 保守方向:reload 后只应用 provider/model 列表和当前默认 model/thinking/permission,不承诺更新 loopControl/background/hooks/services。 +- 完整方向:让 `Session`/`Agent` 支持 `refreshRuntimeConfig(nextConfig)`,替换或更新 `kimiConfig`、permission rules、hook engine、tool services 等。 + +### 缺口 3:MCP reload 需要新 lifecycle + +直接调用 `connectAll(nextServers)` 不够安全,因为删除的 server 不会消失,同名 server 的旧 client 也未显式关闭。需要给 `McpConnectionManager` 增加 `reload(configs)` 或 `replaceAll(configs)`: + +- 对删除项:emit disconnected/disabled 后关闭 client 并移除。 +- 对新增项:按现有 connect 逻辑连接。 +- 对变更项:关闭旧 client,重新连接。 +- 对未变更项:尽量保留连接,避免无谓中断。 + +### 缺口 4:skills/plugin roots 不会自动更新 + +`Session.loadSkills()` 是 private,并且 `skillsReady` 是一次性 Promise。config 中 `extraSkillDirs/mergeAllAvailableSkills` 或插件 skill roots 改动后,当前 session 的 `SkillRegistry` 不会刷新,TUI 的 skill-derived slash commands 也不会更新。 + +需要为 `Session` 增加可调用的 reload skills 能力,完成后 TUI 调 `refreshSkillCommands(session)`。 + +### 缺口 5:services/runtime provider 被缓存 + +`KimiCore.resolveRuntime()` 会缓存 `this.runtime`。配置里 `services.moonshotSearch/moonshotFetch` 改了,现有 toolServices 不会重建,Agent 的 WebSearch/FetchURL 工具也不会刷新。完整热重载需要清空或重建 runtime,并触发 `agent.tools.initializeBuiltinTools()`。 + +### 缺口 6:TUI tui.toml reload 只存在读写函数,没有命令级入口 + +`loadTuiConfig()` 可以读,但 TUI 需要定义应用策略: + +- theme:读取后调用 `applyTheme(next.theme, resolved)`,`auto` 时调用/保留 terminal theme tracking。 +- editor:更新 `appState.editorCommand`。 +- notifications/upgrade:更新 `appState.notifications/upgrade`。 +- 解析失败:沿用 `TuiConfigParseError` 行为,显示错误,不覆盖当前 UI state。 + +## 推荐实现分层 + +### 阶段 1:实现保守可用的 `/config-reload` + +先实现用户最直接可感知的热重载,不承诺所有 session 构造期配置都能无重启生效。 + +行为建议: + +1. 命令注册: + - 新增 `/config-reload`,availability 建议 `idle-only`。 + - 原因:重载 model/tools/MCP/permission 时如果正 streaming,容易造成当前 turn 状态和工具列表不一致。 + +2. TUI handler: + - 调 `harness.getConfig({ reload: true })` 或未来 `harness.reloadConfig()`。 + - 调 `authFlow.refreshAvailableModels()` 更新 `/model` 列表。 + - 重新读取 `tui.toml` 并应用 theme/editor/notifications/upgrade。 + - 对当前 session: + - 如果新 config 有 `defaultModel` 且存在于 `config.models`,可选择是否自动切到 default model。 + - 更稳妥的策略是“保持当前 runtime model,只刷新可选列表;如果当前 model 已无法解析则提示用户运行 `/model` 选择”。 + - 如果 config 明确设置 `defaultPermissionMode`,是否覆盖当前 session mode 需要产品确认。当前 session 的 `/yolo`/`/auto` 是运行态选择,自动覆盖可能让用户意外改变审批策略。 + +3. 状态提示: + - 输出简短摘要,例如 `Config reloaded: 3 models, 2 providers. TUI settings updated.` + - 如果当前模型不再存在,提示 `Current model alias is no longer configured; run /model to choose another.` + +阶段 1 能解决: + +- 用户修改 `config.toml` 后,`/model` 能看到新增/删除的模型 alias。 +- 用户修改默认 provider/model 后,后续 `/model`/`setModel` 能按新配置解析。 +- 用户修改 `tui.toml` 后,theme/editor/notifications/upgrade 能反映到 TUI。 + +阶段 1 暂不解决: + +- 已连接 MCP server 增删改。 +- 当前 session 的 skills roots 变更。 +- hooks、background、loopControl、services 的完整热替换。 + +### 阶段 2:新增 Core/SDK 正式 reload API + +推荐 API 形状: + +```ts +interface ReloadKimiConfigPayload { + readonly sessionId?: string; +} + +interface ReloadKimiConfigResult { + readonly config: KimiConfig; + readonly models: { readonly count: number; readonly defaultModel?: string }; + readonly providers: { readonly count: number }; + readonly sessions: readonly { + readonly id: string; + readonly model?: string; + readonly modelValid: boolean; + readonly skillsReloaded: boolean; + readonly mcpReloaded: boolean; + }[]; + readonly warnings: readonly string[]; +} +``` + +Core 负责: + +- 重新 `loadRuntimeConfig()`。 +- 清理/重建 `runtime` 或比较 services 后按需重建。 +- 对目标 active session 调用 `session.reloadConfig(nextConfig, derivedOptions)`。 +- 返回结构化结果给 TUI,而不是让 TUI 猜测发生了什么。 + +SDK 负责: + +- `KimiHarness.reloadConfig()` +- 可选 `Session.reloadConfig()`,只刷新当前 session。 + +TUI 负责: + +- 调 SDK API。 +- 调 `authFlow.refreshAvailableModels()`。 +- 调 `syncRuntimeState()` 和 `refreshSkillCommands()`。 +- 展示摘要和 warnings。 + +### 阶段 3:完整 session runtime reload + +需要新增或调整: + +- `Session.reloadRuntimeConfig(input)`: + - 替换 hooks:需要 `HookEngine` 支持更新 hooks,或者重建并让 Agent 引用新 engine。 + - 替换 permission rules:`PermissionManager` 增加 `setRules()`,保留 session-runtime approval rules。 + - 重载 skills:`SkillRegistry` 支持重新 load roots,并保留/处理已激活 skill 的状态。 + - 重载 MCP:`McpConnectionManager.reload(nextServers)`。 + - 更新 background config:已有 background task 不强制杀掉,新 task 用新配置。 + - 重建 toolServices 并重新 `initializeBuiltinTools()`。 + - 重新解析当前 model alias;当前 alias 消失时保留显示但返回 warning,避免静默切模型。 + +## 命令语义建议 + +`/config-reload` 默认建议只 reload,不写回配置文件。 + +建议用户可见行为: + +- 成功:`Config reloaded. Models: N, providers: M.` +- TUI 偏好变更:追加 `TUI settings updated.` +- 当前模型失效:追加 warning,要求用户 `/model`。 +- 配置解析失败:显示 `Error: Invalid configuration ...`,保持当前运行态不变。 +- `tui.toml` 解析失败:显示 TUI config error,保持当前 TUI state 不变;Core config reload 不受影响。 + +需要产品确认的点: + +1. 是否要在 `/config-reload` 后自动切到新的 `default_model`? + - 推荐:不自动切。当前 session 的 model 是运行态选择,自动切换会改变后续请求行为。 + +2. 是否要自动应用新的 `default_permission_mode`? + - 推荐:不自动覆盖当前 session mode。审批模式属于安全敏感运行态,用户可以用 `/permission`、`/auto`、`/yolo` 显式切换。 + +3. 是否包含 `mcp.json` 和插件 reload? + - 推荐:阶段 1 不包含;阶段 2/3 在 Core API 中返回明确结果。否则容易出现“状态显示已 reload,但工具实际没有变化”的半成功体验。 + +## 测试建议 + +### Agent Core + +新增/扩展: + +- `packages/agent-core/test/harness/model-alias-session.test.ts` + - 覆盖外部修改 `config.toml` 后 reload API 更新 Core config。 + - 覆盖当前 session 保持旧 model alias,不自动切到新 default。 + - 覆盖 current alias 仍存在但 provider 配置改变后,下一次 `getConfig/getStatus` 能看到新 provider/modelCapabilities。 + - 覆盖 invalid config reload 失败且旧 config 保留。 + +- MCP 完整 reload 若进入实现: + - `packages/agent-core/test/mcp/connection-manager.test.ts` + - 覆盖新增、删除、变更、未变更 server。 + +### Node SDK + +新增/扩展: + +- `packages/node-sdk/test/config.test.ts` 或单独近邻现有 config 测试 + - `harness.reloadConfig()` 返回结构化结果。 + - `session.reloadConfig()` 若实现,关闭 session 后调用应符合 `session.closed` 行为。 + +### TUI + +扩展: + +- `apps/kimi-code/test/tui/commands/registry.test.ts` + - `/config-reload` 能被识别,availability 符合预期。 + +- `apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts` + - 输入 `/config-reload` 后调用 `getConfig({ reload: true })` 或 `reloadConfig()`。 + - `availableModels/availableProviders` 更新,`/model` picker 看到新 alias。 + - 正在 streaming 时命令被 idle-only 逻辑阻止。 + - `tui.toml` reload 成功时 appState theme/editor/notifications/upgrade 更新。 + - reload 失败时显示错误,旧 appState 不变。 + +## 风险与边界 + +- 热重载不是简单“重读文件”:当前 Core 有一部分配置是动态读取,一部分是 session/agent 构造期固定。 +- 自动应用默认模型和默认权限会改变当前会话行为,尤其权限属于安全敏感状态,建议不要隐式覆盖。 +- MCP reload 涉及外部进程/HTTP 连接生命周期,必须有明确 diff/关闭策略,不能直接复用 `connectAll()`。 +- skills reload 可能改变 slash autocomplete 和 Skill tool 可见范围;需要同步 TUI `refreshSkillCommands()`。 +- services reload 会改变 WebSearch/FetchURL 工具背后的 provider,需要清理 `KimiCore.runtime` 缓存并重新初始化 builtin tools。 +- `KIMI_MODEL_*` 环境变量是运行时配置的一部分;`/config-reload` 重读 `config.toml` 时仍会叠加这些 env overrides,文案上不应暗示只来自磁盘文件。 + +## 结论 + +现有代码已经支持“重新读取 Core config 并刷新模型列表”的一部分能力,但还没有一个能把配置变更完整应用到已存在 Session/Agent 的 reload API。推荐先实现保守版 `/config-reload`:刷新 `config.toml`、刷新 TUI 可用模型/provider、重读并应用 `tui.toml`、同步当前 session 状态并展示 warning。随后再补 Core/SDK 的正式 reload API,把 MCP、skills、permission rules、services、hooks/background 等构造期配置纳入可测试的 session runtime reload 流程。 From 51c70c5425ec1f0194316f46270472267aa93f28 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 3 Jun 2026 20:32:04 +0800 Subject: [PATCH 2/4] feat: add reload commands for session and tui --- .changeset/reload-session-tui.md | 7 + apps/kimi-code/src/tui/commands/dispatch.ts | 9 + apps/kimi-code/src/tui/commands/index.ts | 1 + apps/kimi-code/src/tui/commands/registry.ts | 14 + apps/kimi-code/src/tui/commands/reload.ts | 52 ++ apps/kimi-code/src/tui/kimi-tui.ts | 28 + .../test/tui/commands/registry.test.ts | 12 + .../test/tui/commands/reload.test.ts | 139 ++++ .../test/tui/commands/resolve.test.ts | 20 + .../test/tui/kimi-tui-message-flow.test.ts | 52 ++ packages/agent-core/src/rpc/core-api.ts | 5 + packages/agent-core/src/rpc/core-impl.ts | 30 + packages/agent-core/src/session/index.ts | 15 + .../agent-core/test/harness/runtime.test.ts | 90 +++ .../test/session/lifecycle-hooks.test.ts | 36 + packages/node-sdk/src/kimi-harness.ts | 25 + packages/node-sdk/src/rpc.ts | 5 + packages/node-sdk/src/session.ts | 13 +- packages/node-sdk/test/config.test.ts | 22 + plan.md | 678 ++++++++++++++++++ 20 files changed, 1251 insertions(+), 2 deletions(-) create mode 100644 .changeset/reload-session-tui.md create mode 100644 apps/kimi-code/src/tui/commands/reload.ts create mode 100644 apps/kimi-code/test/tui/commands/reload.test.ts create mode 100644 plan.md diff --git a/.changeset/reload-session-tui.md b/.changeset/reload-session-tui.md new file mode 100644 index 0000000000..364f384553 --- /dev/null +++ b/.changeset/reload-session-tui.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code-sdk": minor +"@moonshot-ai/kimi-code": minor +--- + +Add /reload to reload the current session and apply updated config files, plus /reload-tui to reload only TUI preferences. diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index 259fb02d9d..c133adde3d 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -37,6 +37,7 @@ import { import { handleProviderCommand } from './provider'; import { handleFeedbackCommand, showMcpServers, showStatusReport, showUsage } from './info'; import { handlePluginsCommand } from './plugins'; +import { handleReloadCommand, handleReloadTuiCommand } from './reload'; import { handleExportDebugZipCommand, handleExportMdCommand, @@ -73,6 +74,7 @@ export { showUsage, } from './info'; export { handlePluginsCommand } from './plugins'; +export { handleReloadCommand, handleReloadTuiCommand } from './reload'; export { handleExportDebugZipCommand, handleExportMdCommand, @@ -105,6 +107,7 @@ export interface SlashCommandHost { // Session requireSession(): Session; switchToSession(session: Session, message: string): Promise; + reloadCurrentSessionView(session: Session, message: string): Promise; beginSessionRequest(): void; failSessionRequest(message: string): void; sendQueuedMessage(session: Session, item: QueuedMessage): void; @@ -228,6 +231,12 @@ async function handleBuiltInSlashCommand( case 'plugins': void handlePluginsCommand(host, args); return; + case 'reload': + await handleReloadCommand(host); + return; + case 'reload-tui': + await handleReloadTuiCommand(host); + return; case 'editor': await handleEditorCommand(host, args); return; diff --git a/apps/kimi-code/src/tui/commands/index.ts b/apps/kimi-code/src/tui/commands/index.ts index 643856a728..3768d1f628 100644 --- a/apps/kimi-code/src/tui/commands/index.ts +++ b/apps/kimi-code/src/tui/commands/index.ts @@ -28,6 +28,7 @@ export { showUsage, } from './info'; export { handlePluginsCommand } from './plugins'; +export { handleReloadCommand, handleReloadTuiCommand } from './reload'; export { handleForkCommand, handleInitCommand, diff --git a/apps/kimi-code/src/tui/commands/registry.ts b/apps/kimi-code/src/tui/commands/registry.ts index 71001d4ad0..706f241434 100644 --- a/apps/kimi-code/src/tui/commands/registry.ts +++ b/apps/kimi-code/src/tui/commands/registry.ts @@ -90,6 +90,20 @@ export const BUILTIN_SLASH_COMMANDS = [ priority: 60, availability: 'always', }, + { + name: 'reload', + aliases: [], + description: 'Reload session and apply config.toml settings plus tui.toml UI preferences', + priority: 60, + availability: 'idle-only', + }, + { + name: 'reload-tui', + aliases: [], + description: 'Reload only tui.toml UI preferences', + priority: 60, + availability: 'always', + }, { name: 'compact', aliases: [], diff --git a/apps/kimi-code/src/tui/commands/reload.ts b/apps/kimi-code/src/tui/commands/reload.ts new file mode 100644 index 0000000000..402c07765e --- /dev/null +++ b/apps/kimi-code/src/tui/commands/reload.ts @@ -0,0 +1,52 @@ +import type { KimiConfig } from '@moonshot-ai/kimi-code-sdk'; + +import { loadTuiConfig, type TuiConfig } from '../config'; +import type { SlashCommandHost } from './dispatch'; + +export async function handleReloadTuiCommand(host: SlashCommandHost): Promise { + const tuiConfig = await loadTuiConfig(); + applyReloadedTuiConfig(host, tuiConfig); + host.showStatus('TUI config reloaded.', host.state.theme.colors.success); +} + +export async function handleReloadCommand(host: SlashCommandHost): Promise { + const tuiConfig = await loadTuiConfig(); + const session = host.session; + + if (session !== undefined) { + await session.reloadSession(); + await host.reloadCurrentSessionView(session, 'Session reloaded.'); + } + + const config = await host.harness.getConfig({ reload: true }); + applyRuntimeConfig(host, config); + applyReloadedTuiConfig(host, tuiConfig); + + if (session === undefined) { + host.showStatus( + 'Runtime and TUI config reloaded; no active session.', + host.state.theme.colors.success, + ); + } +} + +export function applyReloadedTuiConfig( + host: SlashCommandHost, + config: TuiConfig, +): void { + const resolved = config.theme === 'auto' ? host.state.theme.resolvedTheme : config.theme; + host.applyTheme(config.theme, resolved); + host.refreshTerminalThemeTracking(); + host.setAppState({ + editorCommand: config.editorCommand, + notifications: config.notifications, + upgrade: config.upgrade, + }); +} + +function applyRuntimeConfig(host: SlashCommandHost, config: KimiConfig): void { + host.setAppState({ + availableModels: config.models ?? {}, + availableProviders: config.providers ?? {}, + }); +} diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 3a10aa71de..0f9b018a3a 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1144,6 +1144,34 @@ export class KimiTUI { this.showStatus(statusMessage); } + async reloadCurrentSessionView(session: Session, statusMessage: string): Promise { + this.sessionEventUnsubscribe?.(); + this.sessionEventUnsubscribe = undefined; + this.clearReverseRpcPanels(); + session.setApprovalHandler(undefined); + session.setQuestionHandler(undefined); + this.approvalController.cancelAll('reloading session'); + this.questionController.cancelAll('reloading session'); + + this.resetSessionRuntime(); + this.session = session; + this.harness.setTelemetryContext({ sessionId: session.id }); + this.registerSessionHandlers(session); + await this.syncRuntimeState(session); + this.refreshSessionTitle(); + try { + await this.refreshSkillCommands(session); + } catch { + /* keep the reloaded session usable even if dynamic skills fail */ + } + this.sessionEventHandler.startSubscription(); + const resumeState = session.getResumeState(); + if (resumeState?.warning !== undefined) { + this.showStatus(`Warning: ${resumeState.warning}`, this.state.theme.colors.warning); + } + this.showStatus(statusMessage); + } + async createNewSession(): Promise { if (this.state.appState.isReplaying) { this.showError('Cannot start a new session while history is replaying.'); diff --git a/apps/kimi-code/test/tui/commands/registry.test.ts b/apps/kimi-code/test/tui/commands/registry.test.ts index c481aa2cb1..102a817484 100644 --- a/apps/kimi-code/test/tui/commands/registry.test.ts +++ b/apps/kimi-code/test/tui/commands/registry.test.ts @@ -92,6 +92,8 @@ describe('built-in slash command registry', () => { 'new', 'permission', 'plan', + 'reload', + 'reload-tui', 'sessions', 'settings', 'status', @@ -104,4 +106,14 @@ describe('built-in slash command registry', () => { ]), ); }); + + it('keeps TUI reload always available and full reload idle-only', () => { + const reload = findBuiltInSlashCommand('reload'); + const reloadTui = findBuiltInSlashCommand('reload-tui'); + + expect(reload).toBeDefined(); + expect(reloadTui).toBeDefined(); + expect(resolveSlashCommandAvailability(reload!, '')).toBe('idle-only'); + expect(resolveSlashCommandAvailability(reloadTui!, '')).toBe('always'); + }); }); diff --git a/apps/kimi-code/test/tui/commands/reload.test.ts b/apps/kimi-code/test/tui/commands/reload.test.ts new file mode 100644 index 0000000000..717297df10 --- /dev/null +++ b/apps/kimi-code/test/tui/commands/reload.test.ts @@ -0,0 +1,139 @@ +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { + handleReloadCommand, + handleReloadTuiCommand, +} from '#/tui/commands/reload'; +import type { SlashCommandHost } from '#/tui/commands'; + +const tempDirs: string[] = []; +const originalKimiCodeHome = process.env['KIMI_CODE_HOME']; + +afterEach(async () => { + for (const dir of tempDirs.splice(0)) { + await rm(dir, { recursive: true, force: true }); + } + if (originalKimiCodeHome === undefined) { + delete process.env['KIMI_CODE_HOME']; + } else { + process.env['KIMI_CODE_HOME'] = originalKimiCodeHome; + } +}); + +describe('reload slash commands', () => { + it('reloads tui.toml without touching Core session state', async () => { + await writeTuiConfig(` +theme = "light" + +[editor] +command = "vim" + +[notifications] +enabled = false +notification_condition = "always" + +[upgrade] +auto_install = false +`); + const session = { reloadSession: vi.fn() }; + const host = makeHost({ session }); + + await handleReloadTuiCommand(host); + + expect(host.harness.getConfig).not.toHaveBeenCalled(); + expect(session.reloadSession).not.toHaveBeenCalled(); + expect(host.state.appState).toMatchObject({ + theme: 'light', + editorCommand: 'vim', + notifications: { enabled: false, condition: 'always' }, + upgrade: { autoInstall: false }, + }); + expect(host.showStatus).toHaveBeenCalledWith( + 'TUI config reloaded.', + host.state.theme.colors.success, + ); + }); + + it('reloads the active session, refreshes runtime config, and applies tui.toml', async () => { + await writeTuiConfig('theme = "light"\n'); + const session = { id: 'ses-1', reloadSession: vi.fn(async () => ({})) }; + const host = makeHost({ session }); + + await handleReloadCommand(host); + + expect(session.reloadSession).toHaveBeenCalledOnce(); + expect(host.reloadCurrentSessionView).toHaveBeenCalledWith( + session, + 'Session reloaded.', + ); + expect(host.harness.getConfig).toHaveBeenCalledWith({ reload: true }); + expect(host.state.appState.theme).toBe('light'); + expect(host.state.appState.availableModels).toEqual({ + fresh: { provider: 'test', model: 'fresh-model', maxContextSize: 1000 }, + }); + }); +}); + +async function writeTuiConfig(text: string): Promise { + const dir = join(tmpdir(), `kimi-tui-reload-${Date.now()}-${Math.random().toString(36).slice(2)}`); + tempDirs.push(dir); + await mkdir(dir, { recursive: true }); + process.env['KIMI_CODE_HOME'] = dir; + await writeFile(join(dir, 'tui.toml'), text, 'utf-8'); +} + +function makeHost({ + session, +}: { + readonly session?: Record; +} = {}) { + const state = { + appState: { + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + upgrade: { autoInstall: true }, + availableModels: {}, + availableProviders: {}, + }, + theme: { + resolvedTheme: 'dark', + colors: { + success: '#00ff00', + }, + }, + }; + return { + state, + session, + harness: { + getConfig: vi.fn(async () => ({ + models: { + fresh: { provider: 'test', model: 'fresh-model', maxContextSize: 1000 }, + }, + providers: { + test: { type: 'kimi', apiKey: 'test-key' }, + }, + })), + }, + setAppState: vi.fn((patch: Record) => { + Object.assign(state.appState, patch); + }), + applyTheme: vi.fn((theme: string) => { + state.appState.theme = theme; + }), + refreshTerminalThemeTracking: vi.fn(), + reloadCurrentSessionView: vi.fn(async () => {}), + showStatus: vi.fn(), + } as unknown as SlashCommandHost & { + readonly harness: { + readonly getConfig: ReturnType; + }; + readonly reloadCurrentSessionView: ReturnType; + readonly showStatus: ReturnType; + }; +} diff --git a/apps/kimi-code/test/tui/commands/resolve.test.ts b/apps/kimi-code/test/tui/commands/resolve.test.ts index dbbdff7b7c..b82121ffc3 100644 --- a/apps/kimi-code/test/tui/commands/resolve.test.ts +++ b/apps/kimi-code/test/tui/commands/resolve.test.ts @@ -63,6 +63,11 @@ describe('resolveSlashCommandInput', () => { commandName: 'undo', reason: 'streaming', }); + expect(resolve('/reload', { isStreaming: true })).toEqual({ + kind: 'blocked', + commandName: 'reload', + reason: 'streaming', + }); }); it('blocks model and session pickers while compacting', () => { @@ -76,6 +81,11 @@ describe('resolveSlashCommandInput', () => { commandName: 'resume', reason: 'compacting', }); + expect(resolve('/reload', { isCompacting: true })).toEqual({ + kind: 'blocked', + commandName: 'reload', + reason: 'compacting', + }); }); it('allows always-available built-ins while streaming', () => { @@ -94,6 +104,16 @@ describe('resolveSlashCommandInput', () => { name: 'mcp', args: '', }); + expect(resolve('/reload-tui', { isStreaming: true })).toMatchObject({ + kind: 'builtin', + name: 'reload-tui', + args: '', + }); + expect(resolve('/reload-tui', { isCompacting: true })).toMatchObject({ + kind: 'builtin', + name: 'reload-tui', + args: '', + }); }); it('blocks plan clear while compacting because it is idle-only', () => { diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 74abf124c0..85d26b7d1d 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -169,6 +169,7 @@ function makeSession(overrides: Record = {}) { setPluginMcpServerEnabled: vi.fn(async () => {}), removePlugin: vi.fn(async () => {}), reloadPlugins: vi.fn(async () => ({ added: [], removed: [], errors: [] })), + reloadSession: vi.fn(async () => ({})), getPluginInfo: vi.fn(async (id: string) => ({ id, displayName: id, @@ -334,6 +335,57 @@ describe('KimiTUI message flow', () => { expect(harness.track).toHaveBeenCalledWith('theme_switch', { theme: 'light' }); }); + it('dispatches /reload-tui without reloading the active session', async () => { + const homeDir = await makeTempHome(); + process.env['KIMI_CODE_HOME'] = homeDir; + await writeFile( + join(homeDir, 'tui.toml'), + ` +theme = "light" + +[editor] +command = "vim" +`, + 'utf-8', + ); + const { driver, session, harness } = await makeDriver(); + harness.track.mockClear(); + session.reloadSession.mockClear(); + + driver.handleUserInput('/reload-tui'); + + await vi.waitFor(() => { + expect(driver.state.appState.theme).toBe('light'); + }); + expect(driver.state.appState.editorCommand).toBe('vim'); + expect(session.reloadSession).not.toHaveBeenCalled(); + expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'reload-tui' }); + }); + + it('dispatches /reload through session reload and applies tui.toml', async () => { + const homeDir = await makeTempHome(); + process.env['KIMI_CODE_HOME'] = homeDir; + await writeFile(join(homeDir, 'tui.toml'), 'theme = "light"\n', 'utf-8'); + const { driver, session, harness } = await makeDriver(); + harness.track.mockClear(); + session.reloadSession.mockClear(); + driver.handleUserInput('hello before reload'); + driver.state.appState.streamingPhase = 'idle'; + + driver.handleUserInput('/reload'); + + await vi.waitFor(() => { + expect(session.reloadSession).toHaveBeenCalledOnce(); + }); + await vi.waitFor(() => { + expect(driver.state.appState.theme).toBe('light'); + }); + expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'reload' }); + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('hello before reload'); + expect(transcript).toContain('Session reloaded.'); + }); + it('tracks successful feedback submissions only after the request succeeds', async () => { const { driver, harness } = await makeDriver( makeSession(), diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index a636e2eecb..96f541d514 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -47,6 +47,10 @@ export interface ResumeSessionPayload { readonly sessionId: string; } +export interface ReloadSessionPayload { + readonly sessionId: string; +} + export interface ForkSessionPayload { readonly sessionId: string; readonly id?: string; @@ -315,6 +319,7 @@ export interface CoreAPI extends SessionAPIWithId { createSession: (payload: CreateSessionPayload) => SessionSummary; closeSession: (payload: CloseSessionPayload) => void; resumeSession: (payload: ResumeSessionPayload) => ResumeSessionResult; + reloadSession: (payload: ReloadSessionPayload) => ResumeSessionResult; forkSession: (payload: ForkSessionPayload) => ResumeSessionResult; listSessions: (payload: ListSessionsPayload) => readonly SessionSummary[]; exportSession: (payload: ExportSessionPayload) => ExportSessionResult; diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 8d220bddb6..074940737a 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -66,6 +66,7 @@ import type { PromptPayload, ReconnectMcpServerPayload, RegisterToolPayload, + ReloadSessionPayload, ReloadPluginsResult, RemoveKimiProviderPayload, RemovePluginPayload, @@ -122,6 +123,7 @@ export class KimiCore implements PromisableMethods { private kaos: Promise; private runtime: ToolServices | undefined; private config: KimiConfig; + private readonly runtimeOverride: ToolServices | undefined; private readonly userHomeDir: string; private readonly kimiRequestHeaders: Record | undefined; private readonly resolveOAuthTokenProvider: OAuthTokenProviderResolver | undefined; @@ -148,6 +150,7 @@ export class KimiCore implements PromisableMethods { } throw error; }); + this.runtimeOverride = options.runtime; this.runtime = options.runtime; this.kimiRequestHeaders = options.kimiRequestHeaders; this.resolveOAuthTokenProvider = options.resolveOAuthTokenProvider; @@ -318,6 +321,28 @@ export class KimiCore implements PromisableMethods { return resumeSessionResult(summary, session, warning); } + async reloadSession(input: ReloadSessionPayload): Promise { + const summary = await this.sessionStore.get(input.sessionId); + const active = this.sessions.get(summary.id); + if (active?.hasActiveTurn === true) { + throw new KimiError( + ErrorCodes.TURN_AGENT_BUSY, + `Session "${summary.id}" cannot be reloaded while a turn is running`, + { details: { sessionId: summary.id } }, + ); + } + + this.reloadProviderManager(); + this.clearRuntimeCache(); + await this.reloadPlugins({}); + + if (active !== undefined) { + await active.closeForReload(); + this.sessions.delete(summary.id); + } + return this.resumeSession({ sessionId: summary.id }); + } + async forkSession(input: ForkSessionPayload): Promise { const source = await this.sessionStore.get(input.sessionId); const active = this.sessions.get(source.id); @@ -708,6 +733,11 @@ export class KimiCore implements PromisableMethods { return this.config = loadRuntimeConfig(this.configPath); } + private clearRuntimeCache(): void { + if (this.runtimeOverride !== undefined) return; + this.runtime = undefined; + } + private async refreshSessionRuntimeConfig( session: Session, config: KimiConfig, diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 53b1f63507..0a53a60e28 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -200,6 +200,21 @@ export class Session { } } + async closeForReload(): Promise { + try { + await Promise.allSettled( + Array.from(this.agents.values(), async (agent) => agent.cron?.stop()), + ); + await this.flushMetadata(); + } finally { + try { + await this.mcp.shutdown(); + } finally { + await this.logHandle?.close(); + } + } + } + private async stopBackgroundTasksOnExit(): Promise { const keepAliveOnExit = resolveConfigValue({ env: process.env, diff --git a/packages/agent-core/test/harness/runtime.test.ts b/packages/agent-core/test/harness/runtime.test.ts index a79600c3a9..b268984350 100644 --- a/packages/agent-core/test/harness/runtime.test.ts +++ b/packages/agent-core/test/harness/runtime.test.ts @@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { createRPC, + ErrorCodes, KimiCore, type ApprovalResponse, type CoreAPI, @@ -123,4 +124,93 @@ max_context_size = 100000 expect(mainAgent?.config.modelAlias).toBe('default-mock'); }); + + it('reloads an active session with fresh runtime services from config.toml', async () => { + tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); + const homeDir = join(tmp, 'home'); + const workDir = join(tmp, 'work'); + const configPath = join(homeDir, 'config.toml'); + await mkdir(homeDir, { recursive: true }); + await mkdir(workDir, { recursive: true }); + await writeFile(configPath, baseModelConfig()); + + const [coreRpc, sdkRpc] = createRPC(); + const core = new KimiCore(coreRpc, { homeDir }); + const rpc = await sdkRpc({ + emitEvent: vi.fn(), + requestApproval: vi.fn(async (): Promise => ({ decision: 'rejected' })), + requestQuestion: vi.fn(async () => null), + toolCall: vi.fn(async () => ({ output: '' })), + }); + + const created = await rpc.createSession({ + id: 'ses_runtime_reload', + workDir, + model: 'default-mock', + }); + const before = core.sessions.get(created.id); + expect(before?.options.toolServices?.webSearcher).toBeUndefined(); + + await writeFile( + configPath, + `${baseModelConfig()} +[services.moonshot_search] +base_url = "https://search.example.test/v1" +`, + ); + + const reloaded = await rpc.reloadSession({ sessionId: created.id }); + const after = core.sessions.get(created.id); + + expect(after).toBeDefined(); + expect(after).not.toBe(before); + expect(after?.options.toolServices?.webSearcher).toBeDefined(); + expect(reloaded.agents['main']).toBeDefined(); + }); + + it('rejects reloadSession while the active session has a running turn', async () => { + tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); + const homeDir = join(tmp, 'home'); + const workDir = join(tmp, 'work'); + await mkdir(homeDir, { recursive: true }); + await mkdir(workDir, { recursive: true }); + await writeFile(join(homeDir, 'config.toml'), baseModelConfig()); + + const [coreRpc, sdkRpc] = createRPC(); + const core = new KimiCore(coreRpc, { homeDir }); + const rpc = await sdkRpc({ + emitEvent: vi.fn(), + requestApproval: vi.fn(async (): Promise => ({ decision: 'rejected' })), + requestQuestion: vi.fn(async () => null), + toolCall: vi.fn(async () => ({ output: '' })), + }); + + const created = await rpc.createSession({ + id: 'ses_runtime_reload_busy', + workDir, + model: 'default-mock', + }); + const active = core.sessions.get(created.id); + const main = active?.agents.get('main'); + vi.spyOn(main!.turn, 'hasActiveTurn', 'get').mockReturnValue(true); + + await expect(rpc.reloadSession({ sessionId: created.id })).rejects.toMatchObject({ + code: ErrorCodes.TURN_AGENT_BUSY, + }); + expect(core.sessions.get(created.id)).toBe(active); + }); }); + +function baseModelConfig(): string { + return `default_model = "default-mock" + +[providers.test] +type = "kimi" +api_key = "test-key" + +[models."default-mock"] +provider = "test" +model = "default-mock" +max_context_size = 100000 +`; +} diff --git a/packages/agent-core/test/session/lifecycle-hooks.test.ts b/packages/agent-core/test/session/lifecycle-hooks.test.ts index a83e2e4ddb..7422272e7f 100644 --- a/packages/agent-core/test/session/lifecycle-hooks.test.ts +++ b/packages/agent-core/test/session/lifecycle-hooks.test.ts @@ -154,6 +154,42 @@ describe('Session lifecycle hooks', () => { expect(killSpy).toHaveBeenCalledWith('SIGTERM'); expect(agent.background.getTask(taskId)?.status).toBe('killed'); }); + + it('keeps background tasks alive and skips SessionEnd hooks when closing for reload', async () => { + const { command, logPath, sessionDir, workDir } = await hookFixture(); + const session = new Session({ + kaos: testKaos.withCwd(workDir), + id: 'session-reload-close', + homedir: sessionDir, + rpc: createSessionRpc(), + skills: { explicitDirs: [join(workDir, 'missing-skills')] }, + background: { keepAliveOnExit: false }, + hooks: [ + { event: 'SessionStart', matcher: 'startup', command, timeout: 5 }, + { event: 'SessionEnd', matcher: 'exit', command, timeout: 5 }, + ], + }); + const agent = await session.createMain(); + const stopSpy = vi.spyOn(agent.cron!, 'stop'); + const { proc, killSpy } = pendingProcess(); + const taskId = agent.background.registerTask( + new ProcessBackgroundTask(proc, 'sleep 60', 'reload keeps alive'), + ); + + await session.closeForReload(); + + expect(stopSpy).toHaveBeenCalledOnce(); + expect(killSpy).not.toHaveBeenCalled(); + expect(agent.background.getTask(taskId)?.status).toBe('running'); + expect(await readHookPayloads(logPath)).toMatchObject([ + { + hook_event_name: 'SessionStart', + session_id: 'session-reload-close', + cwd: workDir, + source: 'startup', + }, + ]); + }); }); async function hookFixture(): Promise<{ diff --git a/packages/node-sdk/src/kimi-harness.ts b/packages/node-sdk/src/kimi-harness.ts index 769cd3a3df..b424aed431 100644 --- a/packages/node-sdk/src/kimi-harness.ts +++ b/packages/node-sdk/src/kimi-harness.ts @@ -140,6 +140,31 @@ export class KimiHarness { return session; } + async reloadSession(input: ResumeSessionInput): Promise { + const id = normalizeSessionId(input.id); + const active = this.activeSessions.get(id); + if (active !== undefined) { + await active.reloadSession(); + this.trackSessionEvent(active.id, 'session_reload'); + return active; + } + + const summary = await this.rpc.reloadSession({ sessionId: id }); + const session = new Session({ + id: summary.id, + workDir: summary.workDir, + summary, + rpc: this.rpc, + onClose: () => { + this.activeSessions.delete(summary.id); + }, + }); + this.activeSessions.set(session.id, session); + this.trackSessionStarted(summary.id, true); + this.trackSessionEvent(session.id, 'session_reload'); + return session; + } + async forkSession(input: ForkSessionInput): Promise { const summary = await this.rpc.forkSession({ id: normalizeSessionId(input.id), diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 0a5e9c2cc6..c9a3519946 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -154,6 +154,11 @@ export class SDKRpcClient { return rpc.resumeSession({ sessionId: input.id }); } + async reloadSession(input: SessionIdRpcInput): Promise { + const rpc = await this.getRpc(); + return rpc.reloadSession({ sessionId: input.sessionId }); + } + async forkSession(input: ForkSessionInput): Promise { const rpc = await this.getRpc(); return rpc.forkSession({ diff --git a/packages/node-sdk/src/session.ts b/packages/node-sdk/src/session.ts index c696157798..66c77b8699 100644 --- a/packages/node-sdk/src/session.ts +++ b/packages/node-sdk/src/session.ts @@ -12,6 +12,7 @@ import type { PromptInput, ReloadSummary, ResumedSessionState, + ResumedSessionSummary, SessionPlan, SessionStatus, SessionSummary, @@ -34,8 +35,8 @@ export interface SessionOptions { export class Session { readonly id: string; readonly workDir: string; - readonly summary?: SessionSummary | undefined; - private readonly resumeState: ResumedSessionState | undefined; + summary?: SessionSummary | undefined; + private resumeState: ResumedSessionState | undefined; private readonly rpc: SDKRpcClient; private readonly onClose?: (() => void | Promise) | undefined; @@ -55,6 +56,14 @@ export class Session { return this.resumeState; } + async reloadSession(): Promise { + this.ensureOpen(); + const summary = await this.rpc.reloadSession({ sessionId: this.id }); + this.summary = summary; + this.resumeState = resumeStateFromSummary(summary); + return summary; + } + onEvent(listener: (event: Event) => void): Unsubscribe { this.ensureOpen(); return this.rpc.onEvent((event) => { diff --git a/packages/node-sdk/test/config.test.ts b/packages/node-sdk/test/config.test.ts index 3518e848d5..bc0b49faa0 100644 --- a/packages/node-sdk/test/config.test.ts +++ b/packages/node-sdk/test/config.test.ts @@ -302,4 +302,26 @@ describe('KimiHarness config API', () => { expect(config.defaultModel).toBeUndefined(); expect(config.defaultThinking).toBeUndefined(); }); + + it('reloads an active session without closing the SDK session wrapper', async () => { + const homeDir = await makeTempDir(); + const workDir = join(homeDir, 'work'); + const configPath = join(homeDir, 'config.toml'); + await writeFile(configPath, COMPLETE_TOML, 'utf-8'); + const harness = new KimiHarness({ homeDir, identity: TEST_IDENTITY }); + const session = await harness.createSession({ + id: 'session-sdk-reload', + workDir, + model: 'kimi-for-coding', + }); + + expect(session.getResumeState()).toBeUndefined(); + + const reloaded = await harness.reloadSession({ id: session.id }); + + expect(reloaded).toBe(session); + expect(harness.getSession(session.id)).toBe(session); + expect(session.getResumeState()?.agents['main']).toBeDefined(); + await expect(session.getStatus()).resolves.toMatchObject({ model: 'kimi-for-coding' }); + }); }); diff --git a/plan.md b/plan.md new file mode 100644 index 0000000000..2b678039da --- /dev/null +++ b/plan.md @@ -0,0 +1,678 @@ +# /reload 与 /reload-tui 实施计划 + +> **给执行该计划的 agent:** 必须按任务逐项执行。推荐使用 `superpowers:subagent-driven-development` 或 `superpowers:executing-plans`。所有步骤用 checkbox(`- [ ]`)跟踪完成状态。 + +**目标:** 新增两个 reload 命令:`/reload-tui` 只重载并应用 `tui.toml`;`/reload` 重载完整 runtime,内部通过 Core 的 `reloadSession({ sessionId })` 重建当前 session runtime 并 resume 同一个 session,同时应用 `tui.toml`。 + +**架构:** TUI-only reload 留在 TUI 层,不触碰 Core session。完整 runtime reload 由 Core 原子地关闭旧 session runtime、重新读取配置、重建同一个 session 并返回 `ResumeSessionResult`;SDK 保持同一个 `Session` wrapper,并更新 wrapper 内的 resume snapshot;TUI 则重置 UI runtime、刷新 skill commands、重新 hydrate transcript,并应用最新 `tui.toml`。 + +**技术栈:** TypeScript monorepo、`@moonshot-ai/agent-core`、`@moonshot-ai/kimi-code-sdk`、`apps/kimi-code` TUI、Vitest、oxlint。 + +--- + +## 已确定产品语义 + +### `/reload-tui` + +- 只读取并应用 `tui.toml`。 +- 不碰 Core。 +- 不重建 session。 +- 不清 transcript。 +- 不影响 MCP / skills / plugins / cron / background。 +- 建议 `availability: 'always'`,因为它只改 TUI 本地状态。 +- 应用范围: + - `theme` + - `editor.command` + - `notifications.enabled` + - `notifications.notification_condition` + - `upgrade.auto_install` + +状态文案: + +```text +TUI config reloaded: theme, editor, notifications +``` + +无变化: + +```text +TUI config reloaded: no changes +``` + +### `/reload` + +- 重载完整 runtime,并同时应用 `tui.toml`。 +- 必须是 `idle-only`。 +- 有 active session 时: + - 先读取 `tui.toml`,失败则不动 runtime; + - Core 执行 `reloadSession({ sessionId })`; + - TUI 应用 `tui.toml`; + - TUI 重置 runtime UI 状态; + - TUI 刷新 skill slash commands; + - TUI 重新 hydrate transcript; + - TUI 重新订阅 session events。 +- 没有 active session 时: + - 重读 Core config; + - 应用 `tui.toml`; + - 刷新 TUI available models/providers; + - 不执行 `reloadSession()`。 +- 允许破坏 prompt cache。 +- 不需要 skills 后续 reminder / injector。session 被重新 resume 后,新的 system prompt / skills / tools 列表通过正常构造路径进入上下文。 +- 当前 model / permission / plan / thinking 不走手写 deferred 提示。它们由 resume replay 和 `refreshSessionRuntimeConfig()` 的现有行为决定。 +- 如果当前 model alias 被删,允许现有 fallback 逻辑切到新的 `default_model`,并通过已有 resume warning / TUI 状态体现。 +- reload 期间必须停止旧 cron scheduler,但 cron 任务不能丢。新 Agent 构造后会新建 CronManager,resume 时从磁盘重新加载 cron tasks。 +- reload 不能按普通 exit 语义杀 background tasks。 +- reload 不触发普通 `SessionEnd(exit)` hook。 + +状态文案: + +```text +Runtime reloaded: session resumed with latest config. +``` + +无 active session: + +```text +Runtime config reloaded; no active session. +``` + +--- + +## 配置生效模型 + +| 来源 | `/reload-tui` | `/reload` | +|---|---|---| +| `tui.toml` | 读取并应用 | 读取并应用 | +| `config.toml` providers/models | 不处理 | Core reload 后 resume 新 session runtime | +| `config.toml` default model / thinking / permission / plan | 不处理 | 由 resume replay + existing refresh fallback 决定 | +| `config.toml` skills 配置 | 不处理 | 新 Session 构造时重新 resolve skill roots | +| `mcp.json` | 不处理 | 新 Session 构造时重新 connect MCP | +| plugin state | 不处理 | Core reload 前 `plugins.reload()`,新 Session 构造时读取最新 plugin runtime | +| hooks / permission rules / loopControl / background | 不处理 | 新 Session 构造时用最新 config 注入 | +| services | 不处理 | Core 清空 `runtime` cache 后按最新 config 重建 | + +--- + +## 文件结构 + +- 修改:`packages/agent-core/src/rpc/core-api.ts` + - 新增 `ReloadSessionPayload` / `ReloadSessionResult`。 + - `CoreAPI` 新增 `reloadSession(payload)`。 +- 修改:`packages/agent-core/src/rpc/core-impl.ts` + - 实现 `KimiCore.reloadSession()`。 + - reload 前清空按 config 创建的 `this.runtime` cache。 + - reload 前刷新 plugin manager。 +- 修改:`packages/agent-core/src/session/index.ts` + - 新增 reload 专用关闭生命周期 `closeForReload()`。 + - 普通 `close()` 仍保留 exit 语义。 +- 修改:`packages/node-sdk/src/types.ts` + - 导出 `ReloadSessionPayload` / `ReloadSessionResult`。 +- 修改:`packages/node-sdk/src/rpc.ts` + - 新增 `reloadSession(input)`。 +- 修改:`packages/node-sdk/src/kimi-harness.ts` + - 新增 `reloadSession(input)`。 + - 无 active session 的 Core config 重读继续使用 `getConfig({ reload: true })`。 +- 修改:`packages/node-sdk/src/session.ts` + - 新增 `reloadSession()`。 + - reload 后更新当前 wrapper 的 summary / resume state。 + - 不关闭当前 wrapper。 +- 新建:`apps/kimi-code/src/tui/commands/reload.ts` + - `handleReloadCommand()`:实现 `/reload`。 + - `handleReloadTuiCommand()`:实现 `/reload-tui`。 + - `applyReloadedTuiConfig()`:可测试的 TUI config 应用 helper。 +- 修改:`apps/kimi-code/src/tui/commands/dispatch.ts` + - 接入 `/reload` 和 `/reload-tui`。 + - 不补 `invalid` 分支。 +- 修改:`apps/kimi-code/src/tui/commands/registry.ts` + - 注册 `/reload` 为 `idle-only`。 + - 注册 `/reload-tui` 为 `always`。 +- 修改:`apps/kimi-code/src/tui/commands/index.ts` + - 导出新的 reload handlers/helpers。 +- 测试: + - `packages/node-sdk/test/config.test.ts` + - `packages/agent-core/test/session/cron-stop-on-close.test.ts` + - `apps/kimi-code/test/tui/config.test.ts` + - `apps/kimi-code/test/tui/commands/registry.test.ts` + - `apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts` + - 必要时新增 `packages/agent-core/test/harness/reload-session.test.ts` + +--- + +## Task 1:Core API 增加 `reloadSession` + +- [ ] **Step 1:写失败测试,证明 SDK 期望存在 `session.reloadSession()`** + +在 `packages/node-sdk/test/config.test.ts` 增加测试:创建 session,修改 `config.toml`,调用 `session.reloadSession()`,断言同一个 session id 仍可用且 `maxContextTokens` 来自新配置。 + +核心断言: + +```ts +const summary = await session.reloadSession(); + +expect(summary.id).toBe(session.id); +await expect(session.getStatus()).resolves.toMatchObject({ + model: 'old-model', + maxContextTokens: 8192, +}); +await expect(harness.getConfig()).resolves.toMatchObject({ + defaultModel: 'new-model', +}); +``` + +- [ ] **Step 2:运行测试,确认失败** + +```bash +pnpm exec vitest run packages/node-sdk/test/config.test.ts +``` + +预期:失败,错误类似 `session.reloadSession is not a function`。 + +- [ ] **Step 3:修改 `packages/agent-core/src/rpc/core-api.ts`** + +新增: + +```ts +export interface ReloadSessionPayload { + readonly sessionId: string; +} + +export type ReloadSessionResult = ResumeSessionResult; +``` + +在 `CoreAPI` 增加: + +```ts +reloadSession: (payload: ReloadSessionPayload) => ReloadSessionResult; +``` + +--- + +## Task 2:实现 `Session.closeForReload()` + +- [ ] **Step 1:写失败测试:reload close 会 stop cron,但不触发 exit hook** + +在 `packages/agent-core/test/session/cron-stop-on-close.test.ts` 增加: + +```ts +it('closeForReload stops cron without running SessionEnd exit hooks', async () => { + const { session, main } = await createCronSessionForTest('session-cron-reload'); + const stopSpy = vi.spyOn(main.cron!, 'stop'); + const hookSpy = vi.spyOn(session.hookEngine, 'trigger'); + + await session.closeForReload(); + + expect(stopSpy).toHaveBeenCalledTimes(1); + expect(hookSpy).not.toHaveBeenCalledWith( + 'SessionEnd', + expect.objectContaining({ matcherValue: 'exit' }), + ); +}); +``` + +如果该测试文件没有 `createCronSessionForTest()`,按现有 `cron-stop-on-close.test.ts` 的 session 构造方式内联创建 session,不要新增多余 test helper 文件。 + +- [ ] **Step 2:写失败测试:reload close 不按 exit 策略杀 background** + +```ts +it('closeForReload does not stop background tasks through exit policy', async () => { + const { session, main } = await createCronSessionForTest('session-reload-background'); + const stopAllSpy = vi.spyOn(main.background, 'stopAll'); + + await session.closeForReload(); + + expect(stopAllSpy).not.toHaveBeenCalledWith('Session closed'); +}); +``` + +- [ ] **Step 3:实现 `closeForReload()`** + +在 `packages/agent-core/src/session/index.ts` 增加: + +```ts +async closeForReload(): Promise { + try { + await Promise.allSettled( + Array.from(this.agents.values(), async (agent) => agent.cron?.stop()), + ); + await this.flushMetadata(); + } finally { + try { + await this.mcp.shutdown(); + } finally { + await this.logHandle?.close(); + } + } +} +``` + +明确不要调用: + +```ts +await this.stopBackgroundTasksOnExit(); +await this.triggerSessionEnd('exit'); +``` + +--- + +## Task 3:实现 `KimiCore.reloadSession()` + +- [ ] **Step 1:写失败测试** + +新增 `packages/agent-core/test/harness/reload-session.test.ts`,覆盖: + +- `reloadSession` 返回同一个 session id; +- 新 session runtime 使用最新 `config.toml`; +- 当前 session wrapper 仍可通过 `getConfig({ agentId: 'main' })` 读取到新 model capabilities; +- plugin reload 失败时本次 reload 失败。 + +- [ ] **Step 2:实现 Core 方法** + +在 `packages/agent-core/src/rpc/core-impl.ts` 增加: + +```ts +async reloadSession({ sessionId }: ReloadSessionPayload): Promise { + const active = this.sessions.get(sessionId); + if (active?.hasActiveTurn) { + throw new KimiError( + ErrorCodes.SESSION_NOT_IDLE, + `Session "${sessionId}" cannot be reloaded while a turn is active.`, + { details: { sessionId } }, + ); + } + + this.reloadProviderManager(); + this.runtime = undefined; + + try { + await this.pluginsReady; + await this.plugins.reload(); + this.pluginsLoadError = undefined; + } catch (error) { + this.pluginsLoadError = error instanceof Error ? error : new Error(String(error)); + throw new KimiError( + ErrorCodes.PLUGIN_LOAD_FAILED, + `Failed to reload plugins: ${this.pluginsLoadError.message}`, + { cause: error, details: { kimiHomeDir: this.homeDir } }, + ); + } + + if (active !== undefined) { + await active.closeForReload(); + this.sessions.delete(sessionId); + } + + return this.resumeSession({ sessionId }); +} +``` + +执行前用 `rg "SESSION_NOT_IDLE|SESSION_BUSY|ACTIVE_TURN" packages/agent-core/src/errors packages/agent-core/src` 确认错误码;如果没有合适错误码,使用现有最接近错误码,不要随意新增兼容错误码。 + +--- + +## Task 4:SDK 暴露 `reloadSession()` + +- [ ] **Step 1:更新 `packages/node-sdk/src/types.ts`** + +导出: + +```ts +ReloadSessionPayload, +ReloadSessionResult, +``` + +- [ ] **Step 2:更新 `packages/node-sdk/src/rpc.ts`** + +新增: + +```ts +async reloadSession(input: SessionIdRpcInput): Promise { + const rpc = await this.getRpc(); + return rpc.reloadSession({ sessionId: input.sessionId }); +} +``` + +- [ ] **Step 3:更新 `packages/node-sdk/src/kimi-harness.ts`** + +新增: + +```ts +async reloadSession(input: ResumeSessionInput): Promise { + const id = normalizeSessionId(input.id); + const session = this.activeSessions.get(id); + if (session === undefined) { + return this.resumeSession({ id }); + } + await session.reloadSession(); + this.trackSessionEvent(session.id, 'session_reload'); + return session; +} +``` + +- [ ] **Step 4:更新 `packages/node-sdk/src/session.ts`** + +把 `summary` / `resumeState` 改成可更新字段: + +```ts +summary?: SessionSummary | undefined; +private resumeState: ResumedSessionState | undefined; +``` + +新增: + +```ts +async reloadSession(): Promise { + this.ensureOpen(); + const summary = await this.rpc.reloadSession({ sessionId: this.id }); + this.summary = summary; + this.resumeState = resumeStateFromSummary(summary); + return summary; +} +``` + +注意:不要 close 当前 SDK wrapper。 + +--- + +## Task 5:TUI 实现 `/reload-tui` 和 `/reload` + +- [ ] **Step 1:新建 `apps/kimi-code/src/tui/commands/reload.ts`** + +导出: + +```ts +export async function handleReloadTuiCommand(host: SlashCommandHost): Promise; +export async function handleReloadCommand(host: SlashCommandHost): Promise; +export function applyReloadedTuiConfig(...): TuiReloadResult; +``` + +- [ ] **Step 2:实现 `/reload-tui`** + +行为: + +```ts +export async function handleReloadTuiCommand(host: SlashCommandHost): Promise { + try { + const tuiConfig = await loadTuiConfig(); + const result = applyReloadedTuiConfig(host, tuiConfig); + host.showStatus(formatTuiReloadStatus(result)); + } catch (error) { + host.showError(`Failed to reload TUI config: ${formatErrorMessage(error)}`); + } +} +``` + +`/reload-tui` 不创建 spinner也可以;如果保持一致,也可以使用短 spinner。关键是不要触碰 `host.session` 和 `host.harness.getConfig()`。 + +- [ ] **Step 3:新增 TUI host 方法** + +在 `SlashCommandHost` 中新增: + +```ts +reloadCurrentSessionView(session: Session): Promise; +``` + +在 `KimiTUI` 实现: + +```ts +async reloadCurrentSessionView(session: Session): Promise { + this.resetSessionRuntime(); + await this.syncRuntimeState(session); + this.refreshSessionTitle(); + try { + await this.refreshSkillCommands(session); + } catch { + /* keep the reloaded session usable even if dynamic skills fail */ + } + this.clearTranscriptAndRedraw(); + try { + await this.sessionReplay.hydrateFromReplay(session); + } finally { + this.sessionEventHandler.startSubscription(); + } +} +``` + +- [ ] **Step 4:实现 `/reload`** + +有 active session: + +```ts +export async function handleReloadCommand(host: SlashCommandHost): Promise { + const spinner = host.showProgressSpinner('Reloading runtime'); + try { + const tuiConfig = await loadTuiConfig(); + const session = host.session; + + if (session !== undefined) { + await session.reloadSession(); + await host.reloadCurrentSessionView(session); + const tui = applyReloadedTuiConfig(host, tuiConfig); + spinner.stop({ ok: true, label: 'Runtime reloaded' }); + host.showStatus(formatSessionReloadStatus(tui)); + return; + } + + const config = await host.harness.getConfig({ reload: true }); + host.setAppState({ + availableModels: config.models ?? {}, + availableProviders: config.providers, + }); + const tui = applyReloadedTuiConfig(host, tuiConfig); + spinner.stop({ ok: true, label: 'Runtime config reloaded' }); + host.showStatus(formatNoSessionReloadStatus(config, tui)); + } catch (error) { + spinner.stop({ ok: false, label: 'Reload failed' }); + host.showError(`Failed to reload runtime: ${formatErrorMessage(error)}`); + } +} +``` + +文案: + +```text +Runtime reloaded: session resumed with latest config. +Runtime config reloaded; no active session. +``` + +- [ ] **Step 5:注册命令** + +`apps/kimi-code/src/tui/commands/registry.ts`: + +```ts +{ + name: 'reload', + aliases: [], + description: 'Reload runtime config and resume the current session', + priority: 60, + availability: 'idle-only', +}, +{ + name: 'reload-tui', + aliases: [], + description: 'Reload tui.toml without restarting the session', + priority: 60, + availability: 'always', +}, +``` + +不要新增旧的 config reload 命令。 + +- [ ] **Step 6:接入 dispatch** + +`apps/kimi-code/src/tui/commands/dispatch.ts`: + +```ts +case 'reload': + await handleReloadCommand(host); + return; +case 'reload-tui': + await handleReloadTuiCommand(host); + return; +``` + +不要补 `invalid` 分支。 + +--- + +## Task 6:测试 TUI 命令行为 + +- [ ] **Step 1:测试 `/reload-tui` 注册为 always** + +`apps/kimi-code/test/tui/commands/registry.test.ts`: + +```ts +it('registers reload-tui as always available and reload as idle-only', () => { + const reload = findBuiltInSlashCommand('reload'); + const reloadTui = findBuiltInSlashCommand('reload-tui'); + + expect(resolveSlashCommandAvailability(reload!, '')).toBe('idle-only'); + expect(resolveSlashCommandAvailability(reloadTui!, '')).toBe('always'); +}); +``` + +- [ ] **Step 2:测试 `/reload-tui` 只应用 TUI config** + +`apps/kimi-code/test/tui/config.test.ts` 保留 `applyReloadedTuiConfig()` 测试,并新增 command-level mock 测试: + +```ts +it('reload-tui applies tui.toml without reloading the session', async () => { + const host = makeReloadCommandHost(); + await handleReloadTuiCommand(host); + + expect(host.session?.reloadSession).not.toHaveBeenCalled(); + expect(host.harness.getConfig).not.toHaveBeenCalled(); + expect(host.showStatus).toHaveBeenCalledWith(expect.stringContaining('TUI config reloaded')); +}); +``` + +- [ ] **Step 3:测试 `/reload` reload session 并 hydrate** + +`apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts`: + +```ts +it('runs /reload by reloading the active session and hydrating replay', async () => { + const session = makeSessionMock(); + session.reloadSession = vi.fn(async () => resumedSummaryFixture); + const host = makeTuiHost({ session }); + + dispatchInput(host, '/reload'); + await flushPromises(); + + expect(session.reloadSession).toHaveBeenCalledTimes(1); + expect(host.reloadCurrentSessionView).toHaveBeenCalledWith(session); + expect(host.showStatus).toHaveBeenCalledWith( + expect.stringContaining('Runtime reloaded'), + ); +}); +``` + +--- + +## Task 7:清理旧命名 + +- [ ] **Step 1:全仓搜索旧命令名** + +```bash +rg -n "config-reload|/config-reload|reload-session|/reload-session" packages apps docs +``` + +预期:没有旧命令名。 + +- [ ] **Step 2:全仓搜索旧自定义 reload API** + +```bash +rg -n "reloadKimiConfig|reloadConfig\\(|ReloadKimiConfig|ConfigReloadApplied|ConfigReloadDeferredItem" packages apps +``` + +预期:没有旧自定义 reload API;`getConfig({ reload: true })` 可以保留。 + +--- + +## Task 8:验证 + +- [ ] **Step 1:聚焦测试** + +```bash +pnpm exec vitest run packages/node-sdk/test/config.test.ts packages/agent-core/test/harness/reload-session.test.ts packages/agent-core/test/session/cron-stop-on-close.test.ts apps/kimi-code/test/tui/config.test.ts apps/kimi-code/test/tui/commands/registry.test.ts apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts apps/kimi-code/test/tui/commands/resolve.test.ts +``` + +- [ ] **Step 2:typecheck** + +```bash +pnpm --filter @moonshot-ai/agent-core run typecheck +pnpm --filter @moonshot-ai/kimi-code-sdk run typecheck +pnpm --filter @moonshot-ai/kimi-code run typecheck +``` + +- [ ] **Step 3:touched-file lint** + +```bash +pnpm exec oxlint --type-aware packages/agent-core/src/rpc/core-api.ts packages/agent-core/src/rpc/core-impl.ts packages/agent-core/src/session/index.ts packages/node-sdk/src/types.ts packages/node-sdk/src/rpc.ts packages/node-sdk/src/kimi-harness.ts packages/node-sdk/src/session.ts apps/kimi-code/src/tui/commands/reload.ts apps/kimi-code/src/tui/commands/dispatch.ts apps/kimi-code/src/tui/commands/registry.ts apps/kimi-code/src/tui/commands/index.ts +``` + +预期:0 errors。`dispatch.ts` 上既有的 `invalid` exhaustive warning 不在本任务修复范围;如果只剩该 warning,记录但不改。 + +- [ ] **Step 4:diff check** + +```bash +git diff --check +``` + +--- + +## 手测方案 + +1. 修改 `tui.toml` 的 `theme`,执行 `/reload-tui`,确认主题立即变化且 transcript 不清空。 +2. 修改 `tui.toml` 的 `editor.command`,执行 `/reload-tui`,再用 Ctrl-G 确认新 editor 生效。 +3. 修改 `config.toml` 中当前 model alias 的 `max_context_size`,执行 `/reload`,确认 context 上限变化。 +4. 修改 provider `base_url` / `api_key`,执行 `/reload`,下一轮请求应使用新 provider config。 +5. 新增一个 skill,执行 `/reload`,确认 slash skill commands 刷新。 +6. 删除一个 skill,执行 `/reload`,确认 slash skill commands 移除。 +7. 修改 `mcp.json`,执行 `/reload`,确认 MCP 重新连接。 +8. 创建 cron job,执行 `/reload`,再让模型调用 `CronList`,确认任务仍存在。 +9. streaming 中执行 `/reload`,确认被 idle-only 阻止。 +10. streaming 中执行 `/reload-tui`,确认可以应用 TUI config。 +11. 故意写坏 plugin state,执行 `/reload`,确认 reload 失败并保留旧 UI session 可见状态。 + +--- + +## 风险与处理 + +- **`/reload-tui` 误触发 session reload。** + - 处理:`handleReloadTuiCommand()` 不访问 `host.session.reloadSession()`,不访问 `host.harness.getConfig()`。 +- **SDK wrapper reload 后仍持旧 resume state。** + - 处理:`Session.reloadSession()` 必须更新 wrapper 内的 `summary` 和 `resumeState`。 +- **旧 wrapper close 误关新 Core session。** + - 处理:TUI `/reload` 不调用 `setSession()`,不 close wrapper,只在同一 wrapper 上 reload。 +- **background task 被 reload 当成 exit 杀掉。** + - 处理:reload 使用 `closeForReload()`,不调用 `stopBackgroundTasksOnExit()`。 +- **cron 停掉后不能恢复。** + - 处理:cron task 已持久化,旧 CronManager stop 后,新 Agent 构造新 CronManager 并 `loadFromDisk()`。 +- **services 仍用旧配置。** + - 处理:`KimiCore.reloadSession()` 必须 `this.runtime = undefined`。 +- **plugin state 没刷新。** + - 处理:`reloadSession()` 在 resume 前调用 `plugins.reload()`;失败则本次 reload 失败。 +- **reload 失败后 session 被删。** + - 处理:优先 reload config/plugins 这类会失败的步骤,再 close old session;close 后失败时给出明确错误并允许普通 resume 恢复。 + +--- + +## 推荐实现顺序 + +1. Core API 类型。 +2. `Session.closeForReload()`。 +3. `KimiCore.reloadSession()`。 +4. SDK `reloadSession()`。 +5. TUI `/reload-tui`。 +6. TUI `/reload`。 +7. 旧命名清理。 +8. 聚焦测试、typecheck、lint。 +9. 手测。 + +--- + +## 建议 Commit Message + +```text +feat(tui): add runtime and tui reload commands +``` From 29519a48765941952d69c5067aad500f1824ff43 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Thu, 4 Jun 2026 13:16:49 +0800 Subject: [PATCH 3/4] Delete CONFIG_RELOAD_RESEARCH.md --- CONFIG_RELOAD_RESEARCH.md | 339 -------------------------------------- 1 file changed, 339 deletions(-) delete mode 100644 CONFIG_RELOAD_RESEARCH.md diff --git a/CONFIG_RELOAD_RESEARCH.md b/CONFIG_RELOAD_RESEARCH.md deleted file mode 100644 index 6572d84109..0000000000 --- a/CONFIG_RELOAD_RESEARCH.md +++ /dev/null @@ -1,339 +0,0 @@ -# /config-reload 调研文档 - -## 背景 - -目标是新增一个 TUI 斜杠命令 `/config-reload`,用于用户手动修改配置文件后,在不重启 kimi-code 的情况下重新加载并应用配置。这里的“配置”至少涉及两类: - -- Core 运行时配置:`~/.kimi-code/config.toml`,由 `packages/agent-core/src/config/*` 解析,影响 provider/model、thinking、permission、hooks、skills、runtime services、background 等。 -- TUI 客户端偏好:`~/.kimi-code/tui.toml`,由 `apps/kimi-code/src/tui/config.ts` 解析,影响 theme、editor、notifications、upgrade。 - -本次只做代码调研和实现建议,不写 TypeScript 实现。 - -## 当前配置加载链路 - -### Core config.toml - -`KimiCore` 构造时解析配置路径并读取一次运行时配置: - -- `packages/agent-core/src/rpc/core-impl.ts:139-159`:解析 `homeDir/configPath`,`this.config = loadRuntimeConfig(this.configPath)`。 -- `packages/agent-core/src/config/toml.ts`:`readConfigFile()` 读取 TOML;`loadRuntimeConfig()` 在磁盘配置上叠加 `KIMI_MODEL_*` 环境变量合成的 provider/model。 -- `packages/agent-core/src/config/schema.ts`:`KimiConfigSchema` 当前覆盖 provider/model/thinking/permission/hooks/services/skills/loop/background/telemetry 等字段。 - -已有的全局配置 RPC: - -- `packages/agent-core/src/rpc/core-impl.ts:382-386`:`getKimiConfig({ reload: true })` 会重新执行 `loadRuntimeConfig()` 并替换 `KimiCore.this.config`。 -- `packages/agent-core/src/rpc/core-impl.ts:389-393`:`setKimiConfig()` 写回文件后也会刷新 `this.config`。 -- `packages/agent-core/src/rpc/core-impl.ts:442-448`:`setModel()` 前会调用 `reloadProviderManager()`,因此同一 session 切换模型时能看到外部编辑后的 provider/model 配置。 - -### Session 创建/恢复时的配置消费 - -创建 session 时会重新读取 Core 配置,并把配置拆开传给 `Session`: - -- `packages/agent-core/src/rpc/core-impl.ts:172-216`:`createSession()` 调用 `reloadProviderManager()`,然后把 `config`、`background`、`hooks`、`permissionRules`、`skills`、`mcpConfig`、`toolServices` 等传进 `Session`。 -- `packages/agent-core/src/rpc/core-impl.ts:230-240`:新 main agent 初始化 `modelAlias/thinkingLevel/defaultPlanMode/permissionMode`。 -- `packages/agent-core/src/rpc/core-impl.ts:711-735`:恢复 session 后会尝试用当前 config 重新校验/刷新模型 alias,但这是 resume 专用逻辑,不是通用热重载入口。 - -`ProviderManager` 是当前最接近热重载的部分: - -- `packages/agent-core/src/rpc/core-impl.ts:677-684`:`ProviderManager` 拿到的是 `config: () => this.config`。 -- 因此当 `KimiCore.this.config` 更新后,已有 Agent 的 provider/model 解析可以看到新配置,但前提是有触发点,例如 `setModel()` 或下一次构造 provider。 - -### Agent/Session 中固定下来的配置 - -大量配置在 `Session` 或 `Agent` 构造时固定,不会因为 `KimiCore.this.config` 被替换而自动更新: - -- `packages/agent-core/src/session/index.ts:112-149`:`Session` 构造时创建 `HookEngine`、`SkillRegistry`、`McpConnectionManager`,并异步 `loadSkills()` / `loadMcpServers()`。 -- `packages/agent-core/src/session/index.ts:325-339`:skills roots 由构造时 `options.skills` 决定。 -- `packages/agent-core/src/session/index.ts:341-364`:MCP server 由构造时 `options.mcpConfig` 决定,只初始 `connectAll()`。 -- `packages/agent-core/src/session/index.ts:413-432`:每个 Agent 构造时拿到 `config: this.options.config`、`toolServices`、`hookEngine`、`mcp`、`permission` 等。 -- `packages/agent-core/src/agent/index.ts:128-141`:`Agent.kimiConfig`、`toolServices`、`mcp`、`hooks` 都是 readonly 引用。 -- `packages/agent-core/src/agent/config/index.ts:51-55`:thinking level 解析依赖 `agent.kimiConfig?.thinking`,已有 Agent 持有的是旧 `KimiConfig` 对象。 -- `packages/agent-core/src/agent/permission/index.ts:37-55`:permission rules 初始化后保存在 `PermissionManager.rules`,当前无公开替换方法。 -- `packages/agent-core/src/rpc/core-impl.ts:655-663`:`resolveRuntime()` 会缓存 `this.runtime`,所以 `services.moonshotSearch/moonshotFetch` 改动不会自动重建 WebSearch/FetchURL provider。 - -### MCP 和插件 - -MCP 配置不是 `config.toml`,而是: - -- `~/.kimi-code/mcp.json` -- `/.kimi-code/mcp.json` - -相关代码: - -- `packages/agent-core/src/mcp/config-loader.ts`:加载 user/project 两处 `mcp.json`,project 覆盖 user。 -- `packages/agent-core/src/mcp/session-config.ts`:`resolveSessionMcpConfig()` 包装成 session 配置。 -- `packages/agent-core/src/rpc/core-impl.ts:686-694`:插件 MCP server 会合并进 session MCP config。 - -当前 MCP 连接管理器的能力: - -- `packages/agent-core/src/mcp/connection-manager.ts:145-186`:`connectAll()` 会把传入配置写进 entries 并连接,但不是安全的 reload API;它不会处理已删除 server,也不会先关闭同名旧 client。 -- `packages/agent-core/src/mcp/connection-manager.ts:189-205`:`reconnect(name)` 只重连已有 server。 -- `packages/agent-core/src/mcp/connection-manager.ts:208-213`:`shutdown()` 可以关闭全部并清空。 - -插件 reload 已存在: - -- `packages/agent-core/src/rpc/core-impl.ts:616-629`:`reloadPlugins()` 只刷新 `PluginManager` 中的插件记录。 -- `packages/agent-core/src/plugin/manager.ts:180-199`:`PluginManager.reload()` 返回 added/removed/errors。 -- 但已存在 session 的 `SkillRegistry`、plugin MCP server、plugin sessionStart 不会因为 `/plugins reload` 自动更新。 - -## TUI 侧链路 - -### Slash 命令注册和分发 - -新增 slash 命令的热路径: - -- `apps/kimi-code/src/tui/commands/registry.ts`:注册命令名、描述、优先级、availability。 -- `apps/kimi-code/src/tui/commands/dispatch.ts`:`handleBuiltInSlashCommand()` 分发到具体 handler。 -- `apps/kimi-code/src/tui/commands/config.ts`:现有 model/theme/editor/permission/settings 等配置类命令集中在这里。 - -### TUI 状态同步 - -启动时: - -- `apps/kimi-code/src/tui/kimi-tui.ts:470-536`:`init()` 先 `authFlow.refreshAvailableModels()`,再 create/resume session。 -- `apps/kimi-code/src/tui/controllers/auth-flow.ts:29-36`:`refreshAvailableModels()` 调用 `harness.getConfig({ reload: true })`,并更新 `availableModels/availableProviders`。 - -运行时状态: - -- `apps/kimi-code/src/tui/kimi-tui.ts:1004-1018`:`syncRuntimeState()` 从 `session.getStatus()` 同步当前 model/thinking/permission/plan/context。 -- `apps/kimi-code/src/tui/kimi-tui.ts:1531-1541`:`applyTheme()` 可立即应用 theme 到 TUI。 -- `apps/kimi-code/src/tui/commands/config.ts:334-344`:`persistModelSelection()` 已会 `getConfig({ reload: true })` 后再写默认模型。 - -TUI 偏好配置: - -- `apps/kimi-code/src/tui/config.ts`:`loadTuiConfig()` / `saveTuiConfig()` 管理 `tui.toml`。 -- `apps/kimi-code/src/cli/run-shell.ts:33-47`:`tui.toml` 只在 shell 启动时读取一次,`theme = "auto"` 也只在启动时解析一次初始 terminal theme。 -- 已有 `/theme`、`/editor`、settings 命令会保存并即时更新局部 TUI state,但没有“重新读取 tui.toml 并应用”的命令。 - -## 已有能力可以直接复用的部分 - -1. 重读 Core config 文件: - - `harness.getConfig({ reload: true })` 已可触发 Core 重新读取 `config.toml`。 - -2. 刷新 TUI 的可选模型列表: - - `authFlow.refreshAvailableModels()` 已封装读取配置并更新 `availableModels/availableProviders`。 - -3. 应用当前 session 的 model/thinking/permission: - - `session.setModel(alias)` 会触发 Core 先刷新配置再解析新 alias。 - - `session.setThinking(level)` / `session.setPermission(mode)` 可更新已有 Agent 的运行态。 - - `syncRuntimeState()` 可重新拉取并同步 footer/status 需要的 session 状态。 - -4. 应用 TUI theme: - - `loadTuiConfig()` 可重新读取 `tui.toml`。 - - `applyTheme()` 可即时更新 UI theme。 - - editor/notifications/upgrade 可以直接 `setAppState()`。 - -## 关键缺口 - -### 缺口 1:没有 Core 级 `reloadConfig` 语义 - -`getKimiConfig({ reload: true })` 只保证 Core 内存配置更新,不表达“应用到当前 session/agent”。如果 TUI 自己拼装 reload 行为,会导致 SDK/API 语义不清,其他客户端也无法复用。 - -建议新增 Core RPC: - -- `reloadKimiConfig(payload): ReloadConfigResult` -- SDK 暴露 `KimiHarness.reloadConfig()` 或 `Session.reloadConfig()`,具体取决于是否允许一次刷新所有 active sessions。 - -### 缺口 2:已有 Agent 持有旧 `KimiConfig` - -已有 Agent 的 `agent.kimiConfig` 是 readonly,thinking 默认值、loopControl 等都基于旧对象。只替换 `KimiCore.this.config` 不足以更新这些状态。 - -可选方向: - -- 保守方向:reload 后只应用 provider/model 列表和当前默认 model/thinking/permission,不承诺更新 loopControl/background/hooks/services。 -- 完整方向:让 `Session`/`Agent` 支持 `refreshRuntimeConfig(nextConfig)`,替换或更新 `kimiConfig`、permission rules、hook engine、tool services 等。 - -### 缺口 3:MCP reload 需要新 lifecycle - -直接调用 `connectAll(nextServers)` 不够安全,因为删除的 server 不会消失,同名 server 的旧 client 也未显式关闭。需要给 `McpConnectionManager` 增加 `reload(configs)` 或 `replaceAll(configs)`: - -- 对删除项:emit disconnected/disabled 后关闭 client 并移除。 -- 对新增项:按现有 connect 逻辑连接。 -- 对变更项:关闭旧 client,重新连接。 -- 对未变更项:尽量保留连接,避免无谓中断。 - -### 缺口 4:skills/plugin roots 不会自动更新 - -`Session.loadSkills()` 是 private,并且 `skillsReady` 是一次性 Promise。config 中 `extraSkillDirs/mergeAllAvailableSkills` 或插件 skill roots 改动后,当前 session 的 `SkillRegistry` 不会刷新,TUI 的 skill-derived slash commands 也不会更新。 - -需要为 `Session` 增加可调用的 reload skills 能力,完成后 TUI 调 `refreshSkillCommands(session)`。 - -### 缺口 5:services/runtime provider 被缓存 - -`KimiCore.resolveRuntime()` 会缓存 `this.runtime`。配置里 `services.moonshotSearch/moonshotFetch` 改了,现有 toolServices 不会重建,Agent 的 WebSearch/FetchURL 工具也不会刷新。完整热重载需要清空或重建 runtime,并触发 `agent.tools.initializeBuiltinTools()`。 - -### 缺口 6:TUI tui.toml reload 只存在读写函数,没有命令级入口 - -`loadTuiConfig()` 可以读,但 TUI 需要定义应用策略: - -- theme:读取后调用 `applyTheme(next.theme, resolved)`,`auto` 时调用/保留 terminal theme tracking。 -- editor:更新 `appState.editorCommand`。 -- notifications/upgrade:更新 `appState.notifications/upgrade`。 -- 解析失败:沿用 `TuiConfigParseError` 行为,显示错误,不覆盖当前 UI state。 - -## 推荐实现分层 - -### 阶段 1:实现保守可用的 `/config-reload` - -先实现用户最直接可感知的热重载,不承诺所有 session 构造期配置都能无重启生效。 - -行为建议: - -1. 命令注册: - - 新增 `/config-reload`,availability 建议 `idle-only`。 - - 原因:重载 model/tools/MCP/permission 时如果正 streaming,容易造成当前 turn 状态和工具列表不一致。 - -2. TUI handler: - - 调 `harness.getConfig({ reload: true })` 或未来 `harness.reloadConfig()`。 - - 调 `authFlow.refreshAvailableModels()` 更新 `/model` 列表。 - - 重新读取 `tui.toml` 并应用 theme/editor/notifications/upgrade。 - - 对当前 session: - - 如果新 config 有 `defaultModel` 且存在于 `config.models`,可选择是否自动切到 default model。 - - 更稳妥的策略是“保持当前 runtime model,只刷新可选列表;如果当前 model 已无法解析则提示用户运行 `/model` 选择”。 - - 如果 config 明确设置 `defaultPermissionMode`,是否覆盖当前 session mode 需要产品确认。当前 session 的 `/yolo`/`/auto` 是运行态选择,自动覆盖可能让用户意外改变审批策略。 - -3. 状态提示: - - 输出简短摘要,例如 `Config reloaded: 3 models, 2 providers. TUI settings updated.` - - 如果当前模型不再存在,提示 `Current model alias is no longer configured; run /model to choose another.` - -阶段 1 能解决: - -- 用户修改 `config.toml` 后,`/model` 能看到新增/删除的模型 alias。 -- 用户修改默认 provider/model 后,后续 `/model`/`setModel` 能按新配置解析。 -- 用户修改 `tui.toml` 后,theme/editor/notifications/upgrade 能反映到 TUI。 - -阶段 1 暂不解决: - -- 已连接 MCP server 增删改。 -- 当前 session 的 skills roots 变更。 -- hooks、background、loopControl、services 的完整热替换。 - -### 阶段 2:新增 Core/SDK 正式 reload API - -推荐 API 形状: - -```ts -interface ReloadKimiConfigPayload { - readonly sessionId?: string; -} - -interface ReloadKimiConfigResult { - readonly config: KimiConfig; - readonly models: { readonly count: number; readonly defaultModel?: string }; - readonly providers: { readonly count: number }; - readonly sessions: readonly { - readonly id: string; - readonly model?: string; - readonly modelValid: boolean; - readonly skillsReloaded: boolean; - readonly mcpReloaded: boolean; - }[]; - readonly warnings: readonly string[]; -} -``` - -Core 负责: - -- 重新 `loadRuntimeConfig()`。 -- 清理/重建 `runtime` 或比较 services 后按需重建。 -- 对目标 active session 调用 `session.reloadConfig(nextConfig, derivedOptions)`。 -- 返回结构化结果给 TUI,而不是让 TUI 猜测发生了什么。 - -SDK 负责: - -- `KimiHarness.reloadConfig()` -- 可选 `Session.reloadConfig()`,只刷新当前 session。 - -TUI 负责: - -- 调 SDK API。 -- 调 `authFlow.refreshAvailableModels()`。 -- 调 `syncRuntimeState()` 和 `refreshSkillCommands()`。 -- 展示摘要和 warnings。 - -### 阶段 3:完整 session runtime reload - -需要新增或调整: - -- `Session.reloadRuntimeConfig(input)`: - - 替换 hooks:需要 `HookEngine` 支持更新 hooks,或者重建并让 Agent 引用新 engine。 - - 替换 permission rules:`PermissionManager` 增加 `setRules()`,保留 session-runtime approval rules。 - - 重载 skills:`SkillRegistry` 支持重新 load roots,并保留/处理已激活 skill 的状态。 - - 重载 MCP:`McpConnectionManager.reload(nextServers)`。 - - 更新 background config:已有 background task 不强制杀掉,新 task 用新配置。 - - 重建 toolServices 并重新 `initializeBuiltinTools()`。 - - 重新解析当前 model alias;当前 alias 消失时保留显示但返回 warning,避免静默切模型。 - -## 命令语义建议 - -`/config-reload` 默认建议只 reload,不写回配置文件。 - -建议用户可见行为: - -- 成功:`Config reloaded. Models: N, providers: M.` -- TUI 偏好变更:追加 `TUI settings updated.` -- 当前模型失效:追加 warning,要求用户 `/model`。 -- 配置解析失败:显示 `Error: Invalid configuration ...`,保持当前运行态不变。 -- `tui.toml` 解析失败:显示 TUI config error,保持当前 TUI state 不变;Core config reload 不受影响。 - -需要产品确认的点: - -1. 是否要在 `/config-reload` 后自动切到新的 `default_model`? - - 推荐:不自动切。当前 session 的 model 是运行态选择,自动切换会改变后续请求行为。 - -2. 是否要自动应用新的 `default_permission_mode`? - - 推荐:不自动覆盖当前 session mode。审批模式属于安全敏感运行态,用户可以用 `/permission`、`/auto`、`/yolo` 显式切换。 - -3. 是否包含 `mcp.json` 和插件 reload? - - 推荐:阶段 1 不包含;阶段 2/3 在 Core API 中返回明确结果。否则容易出现“状态显示已 reload,但工具实际没有变化”的半成功体验。 - -## 测试建议 - -### Agent Core - -新增/扩展: - -- `packages/agent-core/test/harness/model-alias-session.test.ts` - - 覆盖外部修改 `config.toml` 后 reload API 更新 Core config。 - - 覆盖当前 session 保持旧 model alias,不自动切到新 default。 - - 覆盖 current alias 仍存在但 provider 配置改变后,下一次 `getConfig/getStatus` 能看到新 provider/modelCapabilities。 - - 覆盖 invalid config reload 失败且旧 config 保留。 - -- MCP 完整 reload 若进入实现: - - `packages/agent-core/test/mcp/connection-manager.test.ts` - - 覆盖新增、删除、变更、未变更 server。 - -### Node SDK - -新增/扩展: - -- `packages/node-sdk/test/config.test.ts` 或单独近邻现有 config 测试 - - `harness.reloadConfig()` 返回结构化结果。 - - `session.reloadConfig()` 若实现,关闭 session 后调用应符合 `session.closed` 行为。 - -### TUI - -扩展: - -- `apps/kimi-code/test/tui/commands/registry.test.ts` - - `/config-reload` 能被识别,availability 符合预期。 - -- `apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts` - - 输入 `/config-reload` 后调用 `getConfig({ reload: true })` 或 `reloadConfig()`。 - - `availableModels/availableProviders` 更新,`/model` picker 看到新 alias。 - - 正在 streaming 时命令被 idle-only 逻辑阻止。 - - `tui.toml` reload 成功时 appState theme/editor/notifications/upgrade 更新。 - - reload 失败时显示错误,旧 appState 不变。 - -## 风险与边界 - -- 热重载不是简单“重读文件”:当前 Core 有一部分配置是动态读取,一部分是 session/agent 构造期固定。 -- 自动应用默认模型和默认权限会改变当前会话行为,尤其权限属于安全敏感状态,建议不要隐式覆盖。 -- MCP reload 涉及外部进程/HTTP 连接生命周期,必须有明确 diff/关闭策略,不能直接复用 `connectAll()`。 -- skills reload 可能改变 slash autocomplete 和 Skill tool 可见范围;需要同步 TUI `refreshSkillCommands()`。 -- services reload 会改变 WebSearch/FetchURL 工具背后的 provider,需要清理 `KimiCore.runtime` 缓存并重新初始化 builtin tools。 -- `KIMI_MODEL_*` 环境变量是运行时配置的一部分;`/config-reload` 重读 `config.toml` 时仍会叠加这些 env overrides,文案上不应暗示只来自磁盘文件。 - -## 结论 - -现有代码已经支持“重新读取 Core config 并刷新模型列表”的一部分能力,但还没有一个能把配置变更完整应用到已存在 Session/Agent 的 reload API。推荐先实现保守版 `/config-reload`:刷新 `config.toml`、刷新 TUI 可用模型/provider、重读并应用 `tui.toml`、同步当前 session 状态并展示 warning。随后再补 Core/SDK 的正式 reload API,把 MCP、skills、permission rules、services、hooks/background 等构造期配置纳入可测试的 session runtime reload 流程。 From ed23fe091cc87bfea2f4c37bc1e443a8b079b002 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Thu, 4 Jun 2026 13:23:20 +0800 Subject: [PATCH 4/4] fix(tui): refresh terminal title on reload --- apps/kimi-code/src/tui/kimi-tui.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 6087240351..df7b5db073 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1183,7 +1183,7 @@ export class KimiTUI { this.harness.setTelemetryContext({ sessionId: session.id }); this.registerSessionHandlers(session); await this.syncRuntimeState(session); - this.refreshSessionTitle(); + this.updateTerminalTitle(); try { await this.refreshSkillCommands(session); } catch {