diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json new file mode 100644 index 0000000000000000000000000000000000000000..c1000ebb4441345720f374a8c7e03047fd68ed09 --- /dev/null +++ b/.github/pr-title-checker-config.json @@ -0,0 +1,12 @@ +{ + "LABEL": { + "name": "Invalid PR Title", + "color": "B60205" + }, + "CHECKS": { + "regexp": "^(feat|fix|test|refactor|chore|style|docs|perf|build|ci|revert)(\\(.*\\))?:.*" + }, + "MESSAGES": { + "failure": "The PR title is invalid. Please refer to https://www.conventionalcommits.org/en/v1.0.0/ for the convention." + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..eaa61a910a38752c00103b1c55ce99f3fbeb8386 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + +## Related Issue + + + +Resolve #(issue_number) + +## Problem + + + +## What changed + + + +## Checklist + +- [ ] I have read the [CONTRIBUTING](https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md) document. +- [ ] I have linked a related issue (external PRs: the issue must have a maintainer's `/approve`). +- [ ] I have added tests that prove my feature works. +- [ ] Ran `gen-changesets` skill, or this PR needs no changeset. +- [ ] Ran `gen-docs` skill, or this PR needs no doc update. diff --git a/apps/kimi-code/.gitignore b/apps/kimi-code/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..762220ab4c08e3338d27f3e979f7002f25db17cd --- /dev/null +++ b/apps/kimi-code/.gitignore @@ -0,0 +1,11 @@ +# Copied from packages/kimi-core at build time +agents/ + +# Generated at build time by scripts/build-vis-asset.mjs. +# Only the ~150KB base64 VALUE file is ignored; the committed `.d.ts` stub +# next to it keeps `#/generated/vis-web-asset` type-resolvable on a fresh +# clone (before any build has produced the `.ts`). +src/generated/vis-web-asset.ts + +# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs +/native/ diff --git a/apps/kimi-code/AGENTS.md b/apps/kimi-code/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..630252ff330ff8ca6aeb6aaebc99831974019adf --- /dev/null +++ b/apps/kimi-code/AGENTS.md @@ -0,0 +1,72 @@ +# apps/kimi-code Development Guide + +This file only contains rules local to `apps/kimi-code`. For cross-repo rules, see the root `AGENTS.md`. + +> **Writing or modifying the TUI?** Use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`). It covers the architecture orientation, where new features go, test placement, theme mechanics, and the dialog interaction/visual spec (`DESIGN.md`). This file keeps only the map, boundaries, and hard constraints. + +## TUI File Layout + +`apps/kimi-code` is the terminal UI / CLI app. The entry chain is: + +`src/main.ts` -> `src/cli/commands.ts` -> `src/cli/run-shell.ts` -> SDK `KimiHarness` -> `src/tui/kimi-tui.ts` + +Main directories: + +- `src/constant/`: non-copy constants shared by CLI/TUI — product, protocol, paths, terminal control, updates, and so on. +- `src/cli/`: command-line arguments, subcommands, and CLI startup. +- `src/tui/`: the interactive terminal UI. +- `src/tui/kimi-tui.ts`: the `KimiTUI` coordinator — wires state, layout, editor, session, SDK events, and dialogs together, and dispatches slash-command handlers. Heavy logic is delegated to `controllers/`, not accumulated here. +- `src/tui/tui-state.ts`: `TUIState`, `createTUIState`, `createInitialAppState` — the single global UI-state shape. +- `src/tui/controllers/`: independently-testable responsibilities — `session-event-handler` (SDK event routing), `streaming-ui` (streaming render), `session-replay` (resume/replay), `tasks-browser`, `editor-keyboard`, `auth-flow`. +- `src/tui/commands/`: slash command definitions, parsing, ordering, and dynamic skill command generation. +- `src/tui/components/`: pi-tui components, organized by UI type. +- `src/tui/constant/`: non-copy constants reused across TUI modules — symbols, terminal sequences, render sizing, streaming-arg match rules, and so on. +- `src/tui/components/chrome/`: persistent UI chrome — footer, todo panel, welcome, loader, device code. +- `src/tui/components/dialogs/`: selectors, approval panels, question popups, and settings popups that temporarily replace the editor. +- `src/tui/components/editor/`: the custom input box and the file mention provider. +- `src/tui/components/media/`: image, diff, code highlight, and other media displays. +- `src/tui/components/messages/`: message blocks in the transcript — assistant, user, tool call, thinking, usage, subagent, and so on. +- `src/tui/components/panes/`: right-side / activity-area panes such as the activity pane and queue pane. +- `src/tui/reverse-rpc/`: the adapter layer that bridges SDK approval/question callbacks to the UI. +- `src/tui/theme/`: themes, color tokens, style helpers, terminal-background detection, and the pi-tui markdown theme. +- `src/tui/utils/`: TUI-only utility functions. +- `src/utils/`: app-wide utilities — clipboard, git, history, image, process, usage, and so on. + +## Module Responsibilities + +- `cli` only interprets command-line input, assembles startup arguments, and invokes the TUI. Do not put TUI interaction logic into the CLI. +- `KimiTUI` coordinates; it does not accumulate complex business rules. New logic that can be tested independently should be split into `controllers`, `commands`, `components`, `reverse-rpc`, or `utils` first. +- `controllers` own the heavy, independently-testable slices (event routing, streaming render, session replay, tasks browser, editor keyboard, auth). Event-routing and rendering logic belong here, not on the `KimiTUI` class. +- `commands` only owns slash-command declaration, parsing, and the parsed-result types. The actual execution can be dispatched from `KimiTUI`, but complex logic should continue to sink downward. +- `components` only handle presentation and local interaction; they must not call the SDK directly, and must not read or write session state directly. +- `reverse-rpc` converts SDK approval/question requests into the data shape a UI panel/dialog needs, and converts the user's choice back into an SDK response. +- `theme` is the single source of truth for colors and styles. Components must not bypass the theme system and use chalk named colors directly. +- `utils` holds utility functions with no UI-state dependency. Logic that needs `TUIState` or a component instance must not live under app-level `src/utils`. +- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import engine packages directly in app code. + +## TUI Coding Conventions + +- Do not over-encapsulate, especially for one- or two-line functions — do not introduce a two-layer wrapper, just inline. +- Functions with no state / UI side effects do not belong as private methods on the `KimiTUI` class; put them in external utils. +- Constants must live in the corresponding `constant` directory; they must not be scattered through component or logic code. +- Inside `handleInput(data)`, when comparing a printable character (letter, digit, space, punctuation), it is **forbidden** to write literal comparisons such as `data === 'q'`. With the Kitty keyboard protocol enabled in terminals like VSCode, these keys are sent as CSI-u sequences (e.g. `\x1b[113u`), and a bare comparison will never match. Decode with `printableChar(data)` from `src/tui/utils/printable-key.ts` first, then compare; function keys continue to use `matchesKey(data, Key.*)`; control characters (codepoint < 32) may still be compared against the raw `data`. `test/tui/printable-key-guard.test.ts` enforces this in CI. + +## Color Rules (normative) + +The theme apply/switch mechanics live in the `write-tui` skill. The following rules are hard and guard-enforced: + +- Do not use chalk named colors such as `chalk.red`, `chalk.cyan`, `chalk.white`, `chalk.gray`, `chalk.dim`, or `chalk.yellow` directly. +- If a component already has `colors`, use `chalk.hex(colors.)(text)`. +- If a component already has `state.theme.styles` or styles passed in, prefer helpers such as `styles.error(text)`, `styles.dim(text)`. +- When new visual semantics have no token, first add a semantic field to `ColorPalette`, and fill in both `darkColors` and `lightColors`. +- In light themes, text tokens against a white background must be at least 4.5:1; borders and large chrome must be at least 3:1. +- Do not cache styled chalk functions at module top level. Theme switching must take effect within a single render, so styles must be generated on the render path from the current palette. +- Non-comment code must not contain chalk named colors such as `chalk.white`, `chalk.cyan`, `chalk.red`, `chalk.green`, `chalk.gray`, `chalk.yellow`, `chalk.blue`, `chalk.magenta`, `chalk.whiteBright`, or `chalk.blackBright`. `test/tui/chalk-named-color-guard.test.ts` enforces this in CI. + +## General Coding Requirements + +- The startup path before the workspace trust gate (`KimiTUI.start()` -> `maybeRunWorkspaceTrustPrompt()`) must not spawn child processes by bare command name — on Windows, cmd.exe / CreateProcess resolve them from the current directory first, so a binary planted in an untrusted workspace would run before the user confirms trust. When an external command is unavoidable, resolve it with `resolveCommandPath` from `src/utils/process/resolve-command.ts`, which returns an absolute PATH hit and refuses matches inside the cwd. +- For optional object properties, pass `undefined` directly — do not use conditional spread. +- Optional object properties do not need to additionally allow `undefined` in the type. +- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity. +- Except for a package's own `index.ts`, other `index.ts` files should prefer `export * from './module'`. diff --git a/apps/kimi-code/CHANGELOG.md b/apps/kimi-code/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..17652e712a24ce749b9a06060a6a1a41d9de3270 --- /dev/null +++ b/apps/kimi-code/CHANGELOG.md @@ -0,0 +1,2838 @@ +# @moonshot-ai/kimi-code + +## 0.43.1 + +### Patch Changes + +- [#3780](https://github.com/MoonshotAI/kimi-code/pull/3780) [`486dcd2`](https://github.com/MoonshotAI/kimi-code/commit/486dcd26c76f2854fc351515a45d8f0fc2d31ed6) Thanks [@Grapedge](https://github.com/Grapedge)! - Update the terminal UI engine, fixing link colors in wrapped markdown tables and `@` file-completion ordering, and adding native clipboard support on Linux X11. + +- [#3776](https://github.com/MoonshotAI/kimi-code/pull/3776) [`48fad6e`](https://github.com/MoonshotAI/kimi-code/commit/48fad6e7d9cd00511c6118e84ae6d22dafb96be9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix memory not being released when subagent scopes are disposed. + +- [#3777](https://github.com/MoonshotAI/kimi-code/pull/3777) [`dc76b0c`](https://github.com/MoonshotAI/kimi-code/commit/dc76b0caaa6e0aa406bf87957e6b5f8aca722221) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix progressively slower rendering on each round of large agent swarm runs. + +- [#3631](https://github.com/MoonshotAI/kimi-code/pull/3631) [`1336be3`](https://github.com/MoonshotAI/kimi-code/commit/1336be38777959cc558fed2b51dc53caa07da5db) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Stop returning deleted sessions from global search before the search index catches up. + +- [#3775](https://github.com/MoonshotAI/kimi-code/pull/3775) [`f248624`](https://github.com/MoonshotAI/kimi-code/commit/f2486241fb5e268d7a43cb589b5c2d59af8db571) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Reduce event-loop stalls and GC churn in sessions with many concurrent subagents. + +- [#3779](https://github.com/MoonshotAI/kimi-code/pull/3779) [`82ec469`](https://github.com/MoonshotAI/kimi-code/commit/82ec469e47eed52feb664fd361e3d8dcb774596e) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix pressing Ctrl+C while subagents are running exiting the whole CLI instead of just interrupting the subagents. + +- [#3752](https://github.com/MoonshotAI/kimi-code/pull/3752) [`0725ef1`](https://github.com/MoonshotAI/kimi-code/commit/0725ef1ce0a7f9c15db7ffc042f57308657b3ee6) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Fix tower mode mistaking newly spawned agents for previous sessions' roster entries. + +## 0.43.0 + +### Minor Changes + +- [#3749](https://github.com/MoonshotAI/kimi-code/pull/3749) [`6126472`](https://github.com/MoonshotAI/kimi-code/commit/6126472c7af421070f09c470c5750455dd277657) Thanks [@liruifengv](https://github.com/liruifengv)! - web: AI session titles are now always on — a title is generated after the first turn and can be regenerated from the rename field, with no experimental flag required. + +- [#3670](https://github.com/MoonshotAI/kimi-code/pull/3670) [`a9efbe0`](https://github.com/MoonshotAI/kimi-code/commit/a9efbe053f966104d4b51090118da5135c084e22) Thanks [@Grapedge](https://github.com/Grapedge)! - Delete sessions from the session picker: press Ctrl+X on a session, then y to confirm. + +### Patch Changes + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: add user agreement and privacy policy entries to the Settings → About page. + +- [#3750](https://github.com/MoonshotAI/kimi-code/pull/3750) [`775a6c3`](https://github.com/MoonshotAI/kimi-code/commit/775a6c35de40e6dd8cc399cc848f95818733ee7c) Thanks [@7Sageer](https://github.com/7Sageer)! - Add the `loop_control.compaction_max_attempts` config option to set the maximum total attempts for a failing compaction request (default 5). + +- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Add the dynamically_loaded_tools capability to official Kimi Code models when the service declares support for message-level tool declarations. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix sessions containing many brackets or backslashes getting stuck while loading. + +- [#3734](https://github.com/MoonshotAI/kimi-code/pull/3734) [`ee2cac1`](https://github.com/MoonshotAI/kimi-code/commit/ee2cac102b835fcd7adb3d4b9bc3d62b0b71cdfd) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fix a crash that killed the process when a retried LLM request had streamed a partial tool call before disconnecting. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix persistent UI stuttering while streaming in long sessions. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix certain formulas, links, tool results, and log contents causing the UI to stutter or become unresponsive. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the send button staying disabled when starting a new session. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix previous sessions being wrongly marked as unread and triggering "turn complete" notifications after starting a new conversation. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: render Markdown frontmatter metadata as key-value cards and tag lists. + +- [#3688](https://github.com/MoonshotAI/kimi-code/pull/3688) [`9f7e68e`](https://github.com/MoonshotAI/kimi-code/commit/9f7e68e8bdb70d3e5cde5a924740e357ede2fc40) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve MCP attachments that cannot be delivered directly to the model. + +- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Add a per-server `deferred` field to MCP server configuration: when the model supports dynamic tool loading (experimental `tool-select` flag), set `deferred: true` to keep a server's tools out of the top-level tool list and load them on demand via `select_tools`; servers are exposed inline by default. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: drop the Changes entry from the panel new-tab menu while a diff tab is open. + +- [#3657](https://github.com/MoonshotAI/kimi-code/pull/3657) [`5b3b5b6`](https://github.com/MoonshotAI/kimi-code/commit/5b3b5b6f7cfa4e9cca2a394632d52f8748dc8190) Thanks [@chengluyu](https://github.com/chengluyu)! - Exclude time spent with the session closed from goal time budgets. + +- [#3728](https://github.com/MoonshotAI/kimi-code/pull/3728) [`b180725`](https://github.com/MoonshotAI/kimi-code/commit/b1807253c34e12b0ecf60c9b4da3890d0c80ce72) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Add the `KIMI_CODE_PERMISSION_MODE_REMINDER` environment variable: set it to `0` to stop injecting the auto permission-mode reminders into the model context. + +- [#3696](https://github.com/MoonshotAI/kimi-code/pull/3696) [`d3dc594`](https://github.com/MoonshotAI/kimi-code/commit/d3dc5945548d3353a5e6cfc457a3a8cf84d7da71) Thanks [@sailist](https://github.com/sailist)! - Include the server token in the Remote Control Local UI link so it opens already signed in. + +- [#3709](https://github.com/MoonshotAI/kimi-code/pull/3709) [`306f6f6`](https://github.com/MoonshotAI/kimi-code/commit/306f6f69251d65fda434463433c0761758eacb16) Thanks [@sailist](https://github.com/sailist)! - Fix Remote Control uploads larger than ~3.5MB always failing with a 400 error. + +- [#3718](https://github.com/MoonshotAI/kimi-code/pull/3718) [`ab9e688`](https://github.com/MoonshotAI/kimi-code/commit/ab9e688cd0c753bd3cea3406a9e9194c73ab2239) Thanks [@sailist](https://github.com/sailist)! - Reuse unchanged Remote Control assets across page loads instead of retransferring them. + +- [#3657](https://github.com/MoonshotAI/kimi-code/pull/3657) [`5b3b5b6`](https://github.com/MoonshotAI/kimi-code/commit/5b3b5b6f7cfa4e9cca2a394632d52f8748dc8190) Thanks [@chengluyu](https://github.com/chengluyu)! - Remove the 24-hour limit on goal time budgets. + +- [#3714](https://github.com/MoonshotAI/kimi-code/pull/3714) [`565093f`](https://github.com/MoonshotAI/kimi-code/commit/565093f727eee07e2aa05f43859fa12b9e977989) Thanks [@sailist](https://github.com/sailist)! - Skip the confirmation prompt for rm -rf commands that target only /tmp or /temp paths. + +- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Fix the select_tools tool never being registered because agent profiles do not list it in their tool allowlists. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: restructure the Settings pages — Account moves right after General, Agent is renamed to Agents & Sessions and now includes message folding, Advanced is renamed to About, and data & privacy settings move into General. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the selected segment background flashing and shifting while the Settings segmented control loads. + +- [#3697](https://github.com/MoonshotAI/kimi-code/pull/3697) [`5e452fc`](https://github.com/MoonshotAI/kimi-code/commit/5e452fc2ddf5a0d9feedfbf3c7cdde4246fc44b6) Thanks [@RealKai42](https://github.com/RealKai42)! - Allow steering messages to interrupt waits for background tasks. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the switch thumb deforming at both ends when stretched on hover. + +- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix tooltips popping up even when the mouse has not moved. + +- [#3648](https://github.com/MoonshotAI/kimi-code/pull/3648) [`2da4aa2`](https://github.com/MoonshotAI/kimi-code/commit/2da4aa23b0d484312cc068b2d4b2a62e694a2bfe) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode reliability fixes across messaging, worktrees, and the review-to-merge gate. + +- [#3707](https://github.com/MoonshotAI/kimi-code/pull/3707) [`e409bc8`](https://github.com/MoonshotAI/kimi-code/commit/e409bc8ba582c9f390146eca08812d22790c81f8) Thanks [@sailist](https://github.com/sailist)! - Compress Remote Control tunnel responses with gzip. + +- [#3702](https://github.com/MoonshotAI/kimi-code/pull/3702) [`42998cf`](https://github.com/MoonshotAI/kimi-code/commit/42998cfc13cb03914256587f0b702ec95e2d156c) Thanks [@sailist](https://github.com/sailist)! - Add `-y, --yes` to `kimi upgrade` (alias `kimi update`) to skip the confirmation prompt and install the update directly. + +- [#3681](https://github.com/MoonshotAI/kimi-code/pull/3681) [`1306a9a`](https://github.com/MoonshotAI/kimi-code/commit/1306a9a8dbd5acd9532182c36edcabbc7e8de807) Thanks [@7Sageer](https://github.com/7Sageer)! - Warn at startup when a [models] entry in config.toml is missing the model field and cannot be used. + +## 0.42.0 + +### Minor Changes + +- [#3613](https://github.com/MoonshotAI/kimi-code/pull/3613) [`d4d20d2`](https://github.com/MoonshotAI/kimi-code/commit/d4d20d21d733a7942056c15ffcc2c74cce51f7d5) Thanks [@liukx0205](https://github.com/liukx0205)! - Add read-only tools to the /btw side agent. + +- [#3524](https://github.com/MoonshotAI/kimi-code/pull/3524) [`f6a9c39`](https://github.com/MoonshotAI/kimi-code/commit/f6a9c39e22a63b2a231684ed8426bfe4ec406cf7) Thanks [@RealKai42](https://github.com/RealKai42)! - Add an experimental Updates panel with paginated progress messages from the main agent and subagents; enable it with `KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=1`. + +- [#3634](https://github.com/MoonshotAI/kimi-code/pull/3634) [`e831fd1`](https://github.com/MoonshotAI/kimi-code/commit/e831fd1ea9488ad5192bcc9d96579470cf0c4442) Thanks [@7Sageer](https://github.com/7Sageer)! - The subagent model pool (`[secondary_model]`) is now always on; the experimental secondary-model flag and the `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` opt-out have been removed. + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: support permanently deleting sessions from the session row context menu, with a confirmation prompt. + +- [#3526](https://github.com/MoonshotAI/kimi-code/pull/3526) [`55685c5`](https://github.com/MoonshotAI/kimi-code/commit/55685c58b5dbff692e16c66efa65fa6d32222f61) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop reminding the model of its context budget before automatic compaction. + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: preview images and videos in a reorderable media rail in the composer, mention them in the text on demand, and keep the previews after queueing and sending. + +### Patch Changes + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the conversation scrollbar being too thin to click and drag easily. + +- [#3539](https://github.com/MoonshotAI/kimi-code/pull/3539) [`34ad513`](https://github.com/MoonshotAI/kimi-code/commit/34ad5137ce36f6eb03c4a42145e9bdcc8d607d87) Thanks [@RealKai42](https://github.com/RealKai42)! - Collapse finished tool calls in the transcript to a header plus one marked outcome row: short output is shown whole, hidden output is counted (`N more lines`, `+N more`) and revealed by `Ctrl+O`, which the footer advertises while it is available. + +- [#3537](https://github.com/MoonshotAI/kimi-code/pull/3537) [`f12d59e`](https://github.com/MoonshotAI/kimi-code/commit/f12d59e089e2531a33fbca30b26ffeabd5862b45) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix the agent resuming the wrong request after automatic context compaction in long sessions. + +- [#3552](https://github.com/MoonshotAI/kimi-code/pull/3552) [`f0434f2`](https://github.com/MoonshotAI/kimi-code/commit/f0434f2d25ead11d44f5616930157a59dfd524a5) Thanks [@sailist](https://github.com/sailist)! - The minidb session-index read model and global search worker are now always on; the experimental flags have been replaced by the `[database]` config section and the `KIMI_CODE_PERSISTENCE_MINIDB_READMODEL` / `KIMI_CODE_SEARCH_WORKER` env vars. + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix background task notifications lingering at the bottom of the session during subsequent tool calls. + +- [#3658](https://github.com/MoonshotAI/kimi-code/pull/3658) [`ff7371b`](https://github.com/MoonshotAI/kimi-code/commit/ff7371b70a7ae80ec70712b610f73e6d6f29edc0) Thanks [@RealKai42](https://github.com/RealKai42)! - Allow file searches to retrieve matches beyond the first 100 results. + +- [#3652](https://github.com/MoonshotAI/kimi-code/pull/3652) [`7f5debf`](https://github.com/MoonshotAI/kimi-code/commit/7f5debfa71ac9e4a23b5dab1a511aa3672677381) Thanks [@RealKai42](https://github.com/RealKai42)! - Accept HEIC, HEIF, and BMP images on a session's first prompt when the configured default model is served by Kimi and no model has been selected yet. + +- [#3649](https://github.com/MoonshotAI/kimi-code/pull/3649) [`80480c0`](https://github.com/MoonshotAI/kimi-code/commit/80480c01d27a921d4660ad043e78c8b61167f82b) Thanks [@RealKai42](https://github.com/RealKai42)! - Accept HEIC, HEIF, and BMP images in ReadMediaFile and prompt attachments when the model is served by Kimi. + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix page jank caused by hundreds of simultaneous requests when reloading a session with many background tasks. + +- [#3654](https://github.com/MoonshotAI/kimi-code/pull/3654) [`6de0cec`](https://github.com/MoonshotAI/kimi-code/commit/6de0cec174ae3af44fc9f9f74b2aa8599b928fdf) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve distinct structured data in MCP tool results. + +- [#3669](https://github.com/MoonshotAI/kimi-code/pull/3669) [`f8c606e`](https://github.com/MoonshotAI/kimi-code/commit/f8c606e7d7b33a43190721d47ec6c8a2076eaec6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Support compressed downloads from updated native release manifests. + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: reduce jank when opening and scrolling back through long conversations, while preserving message and tool expansion state. + +- [#3548](https://github.com/MoonshotAI/kimi-code/pull/3548) [`baf17a8`](https://github.com/MoonshotAI/kimi-code/commit/baf17a8fcc289f20fa6c8d85dd8f93eeb3ff0cbc) Thanks [@chengluyu](https://github.com/chengluyu)! - Preserve image and video filenames in session history. + +- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Add configurable character limits and resumable long-line file reads without repeated output truncation. + +- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Read malformed UTF-16 files with an explicit lossy-decoding warning. + +- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Avoid repeated scanning for common tail reads and report file changes detected during tail rereads. + +- [#3616](https://github.com/MoonshotAI/kimi-code/pull/3616) [`260ac3f`](https://github.com/MoonshotAI/kimi-code/commit/260ac3faad1cf0ca84dca26173ddf66a09220841) Thanks [@Grapedge](https://github.com/Grapedge)! - Upgrade the default thinking effort to the recommended level for eligible users. + +- [#3552](https://github.com/MoonshotAI/kimi-code/pull/3552) [`f0434f2`](https://github.com/MoonshotAI/kimi-code/commit/f0434f2d25ead11d44f5616930157a59dfd524a5) Thanks [@sailist](https://github.com/sailist)! - Remote Control is now always on; the experimental `KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL` flag has been removed. + +- [#3618](https://github.com/MoonshotAI/kimi-code/pull/3618) [`75682b0`](https://github.com/MoonshotAI/kimi-code/commit/75682b0ef130e0340a9d16421312e2766c327be1) Thanks [@sailist](https://github.com/sailist)! - Fix recent sessions missing from the session list when the sessions directory contains stray files. + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix skills created mid-session not appearing in the slash list until the app restarts. + +- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix trailing backticks briefly flashing at the end of code blocks while they stream. + +- [#3596](https://github.com/MoonshotAI/kimi-code/pull/3596) [`0d7833e`](https://github.com/MoonshotAI/kimi-code/commit/0d7833ee8019d39161f8462977a3a2713bbfd39e) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - The /tasks panel now shows each background agent's model under its task row. + +- [#3607](https://github.com/MoonshotAI/kimi-code/pull/3607) [`5eea890`](https://github.com/MoonshotAI/kimi-code/commit/5eea89016511d113480755907619defafaf025cf) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix the streaming debug timing attributing client-side busy time to the server. + +- [#3596](https://github.com/MoonshotAI/kimi-code/pull/3596) [`0d7833e`](https://github.com/MoonshotAI/kimi-code/commit/0d7833ee8019d39161f8462977a3a2713bbfd39e) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower worker and reviewer briefings now carry the full mission context, and tower agent timeouts follow the subagent timeout setting (`[subagent] timeout_ms` or `KIMI_SUBAGENT_TIMEOUT_MS`), defaulting to 2 hours. Fix the /tasks list not showing the model for tower-spawned agents. + +- [#3593](https://github.com/MoonshotAI/kimi-code/pull/3593) [`00cfbb0`](https://github.com/MoonshotAI/kimi-code/commit/00cfbb0547cee6ec2650fd3fc52e15d2072c41aa) Thanks [@7Sageer](https://github.com/7Sageer)! - Print a warning in `kimi -p` when project-level MCP servers are skipped because the folder is not trusted. + +- [#3608](https://github.com/MoonshotAI/kimi-code/pull/3608) [`fb0353a`](https://github.com/MoonshotAI/kimi-code/commit/fb0353a8ba5ceb7e8ae4e27f3260b3c8c8d80784) Thanks [@liukx0205](https://github.com/liukx0205)! - Watch the user-level skill roots (`~/.kimi-code/skills` and `~/.agents/skills`) so the workspace skill catalog refreshes automatically when skills are created, modified, or deleted while the daemon is running — no restart or manual reload needed. + +- [#3560](https://github.com/MoonshotAI/kimi-code/pull/3560) [`af81bb9`](https://github.com/MoonshotAI/kimi-code/commit/af81bb92215dca2f933579ce0119f7add452bc96) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Open the browser on localhost instead of the wildcard bind address for `kimi web --host 0.0.0.0`. + +- [#3605](https://github.com/MoonshotAI/kimi-code/pull/3605) [`f1e9152`](https://github.com/MoonshotAI/kimi-code/commit/f1e915277655c68213cf91fe5010413bfde3cb65) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix slow response streaming and rendering after resuming sessions with many scheduled cron turns. + +## 0.41.0 + +### Minor Changes + +- [#3423](https://github.com/MoonshotAI/kimi-code/pull/3423) [`b199e33`](https://github.com/MoonshotAI/kimi-code/commit/b199e3326de28d5094ca5f18816a74ab336c51e0) Thanks [@RealKai42](https://github.com/RealKai42)! - Remind the model of its context budget before automatic compaction, and after compaction point it at the session's event log for exact details. + +- [#3525](https://github.com/MoonshotAI/kimi-code/pull/3525) [`eba23ed`](https://github.com/MoonshotAI/kimi-code/commit/eba23edb93ec00aa11ffa288e2b89ebbf3d77a70) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Turn-level file history is now always on; the experimental file-history flag has been removed. + +- [#3516](https://github.com/MoonshotAI/kimi-code/pull/3516) [`6013658`](https://github.com/MoonshotAI/kimi-code/commit/60136588a4218bc2423b93467c4fae8c6554219c) Thanks [@Grapedge](https://github.com/Grapedge)! - Add an occasional session rating prompt above the input box. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: add tower multi-agent collaboration mode (experimental), enabled via the /tower command or the composer plus menu; /tower supports specifying a base branch (e.g. /tower add-new-feature). + +### Patch Changes + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix background agent message cards incorrectly showing a running indicator. + +- [#3529](https://github.com/MoonshotAI/kimi-code/pull/3529) [`b184b31`](https://github.com/MoonshotAI/kimi-code/commit/b184b31497949ac019ac43d5a3dfe2ab87dbebc4) Thanks [@sailist](https://github.com/sailist)! - Stop blocking dangerous commands and commands that cannot be statically analyzed in auto permission mode. + +- [#3522](https://github.com/MoonshotAI/kimi-code/pull/3522) [`523d35b`](https://github.com/MoonshotAI/kimi-code/commit/523d35b54b25a0b4589388a2b6c8c4261f1ef7db) Thanks [@RealKai42](https://github.com/RealKai42)! - Deliver background question answers to the agent directly instead of via a saved output file. + +- [#3522](https://github.com/MoonshotAI/kimi-code/pull/3522) [`523d35b`](https://github.com/MoonshotAI/kimi-code/commit/523d35b54b25a0b4589388a2b6c8c4261f1ef7db) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix background questions being cancelled as soon as the agent finishes its turn. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: polish the style and interaction of Bash commands in the right-side panel. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: support selection comments and quote-to-chat in the diff and per-turn changes panels. + +- [#3459](https://github.com/MoonshotAI/kimi-code/pull/3459) [`b6b9b37`](https://github.com/MoonshotAI/kimi-code/commit/b6b9b374dd5a3cda4257081b194760d7998e73f0) Thanks [@RealKai42](https://github.com/RealKai42)! - Subagent final messages are no longer bounced back for expansion when they are under 200 characters. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix file change previews showing added/removed lines that never existed when the same file is edited multiple times in one turn. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: pressing Esc no longer closes the right detail panel. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix the default thinking effort in settings not being settable to the highest level (Max). + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: rename the three permission modes to Always Ask / Ask When Needed / Never Ask and update their Chinese and English descriptions. + +- [#3473](https://github.com/MoonshotAI/kimi-code/pull/3473) [`d567a6a`](https://github.com/MoonshotAI/kimi-code/commit/d567a6a4fdaeb052deae9d9b313df0d922105799) Thanks [@7Sageer](https://github.com/7Sageer)! - Show a warning after switching to Ask When Needed or Never Ask mode. + +- [#3498](https://github.com/MoonshotAI/kimi-code/pull/3498) [`a3b48a7`](https://github.com/MoonshotAI/kimi-code/commit/a3b48a7272880dafb64e4c403006d94dc781d05c) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix print mode (`kimi -p`) ignoring the `KIMI_DISABLE_TELEMETRY` environment variable. + +- [#3531](https://github.com/MoonshotAI/kimi-code/pull/3531) [`51bd52a`](https://github.com/MoonshotAI/kimi-code/commit/51bd52a589089f99a941e0d3285b0cf638e5b1a0) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix print mode (`kimi -p`) losing session records when the run exits on an error or a termination signal. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: restyle selection quote pills from blue to the same neutral ink as other mentions, with a vertical bar separating the quote and comment. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: shorten selection quote pill excerpts to at most 12 characters. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: restore selection quoting — after selecting text in a message or file preview, you can add a comment or quote it into the chat. + +- [#3478](https://github.com/MoonshotAI/kimi-code/pull/3478) [`052e98e`](https://github.com/MoonshotAI/kimi-code/commit/052e98ec17ac8d931873ef892fb0e1912aa401e0) Thanks [@RealKai42](https://github.com/RealKai42)! - Resuming a subagent by its agent id now works after the session is reopened in a new process; the resumed subagent follows the current permission mode and is matched by its own profile in permission rules. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: selection comment bubbles near the bottom of the page now pop above the selection and grow upward, eliminating double scrollbars. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix selection bubbles displayed over the panel header being unclickable and lacking a hover state. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix the comment bubble popping up before mouse release when selecting text with a slow drag. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: rework selection comment bubble actions into right-aligned Cancel / Add to chat below the input, with an Enter hint on the confirm button. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: revamp the selection comment bubble — the comment box supports multi-line input and auto-grows, and the confirm button now matches the chat composer send button. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: support selection quoting in the terminal — select text to add a comment or quote it into the chat. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: lengthen the hover delay of quote and mention preview cards so merely passing the mouse over them no longer triggers them. + +- [#3461](https://github.com/MoonshotAI/kimi-code/pull/3461) [`8057d30`](https://github.com/MoonshotAI/kimi-code/commit/8057d30afbd2942f7c2e647f33a9dc7401af2b43) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode (experimental, `KIMI_CODE_EXPERIMENTAL_TOWER=1`): fix tower mode never starting when enabled through `[experimental] tower = true` in `config.toml` instead of the environment variable. When tower mode cannot be enabled, the error now names the actual blocker — the disabled experiment, a required restart, or the owning session. When another live session owns the workspace tower, the message also names the owning session's title alongside its id. /tower now also works in a directory that is not a git repository — it runs git init and commits what is there (an empty initial commit for empty directories). + +- [#3521](https://github.com/MoonshotAI/kimi-code/pull/3521) [`744b718`](https://github.com/MoonshotAI/kimi-code/commit/744b718b672199f407786f3b5f789fb3caf4b2a7) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Remove the /dance Easter egg hint from the TUI tips rotation. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix inaccurate added/removed line counts in per-turn file change summaries after the same file is edited or overwritten multiple times. + +- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: per-turn file change cards now show only exact line statistics, and the card no longer appears when statistics are unavailable. + +## 0.40.1 + +### Patch Changes + +- [#3469](https://github.com/MoonshotAI/kimi-code/pull/3469) [`979baad`](https://github.com/MoonshotAI/kimi-code/commit/979baad8597aa1760917752b3663f1eb4e40eeb0) Thanks [@sailist](https://github.com/sailist)! - Fix the condition for showing the kimi-cli migration prompt. + +## 0.40.0 + +### Minor Changes + +- [#3434](https://github.com/MoonshotAI/kimi-code/pull/3434) [`ae7a6dc`](https://github.com/MoonshotAI/kimi-code/commit/ae7a6dc6fb56cde119f0ac1512649a52c19ef7e8) Thanks [@sailist](https://github.com/sailist)! - The `kimi acp` subcommand no longer honors `KIMI_CODE_LEGACY_FLAG`; it always runs on the default agent engine. + +- [#3334](https://github.com/MoonshotAI/kimi-code/pull/3334) [`971a8b2`](https://github.com/MoonshotAI/kimi-code/commit/971a8b24c172912f100eaa9a88625387086b327b) Thanks [@7Sageer](https://github.com/7Sageer)! - The subagent model pool (`[secondary_model]`) is enabled by default in every launch mode and remains opt-out via `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=0` or `[experimental] secondary-model = false`. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Added a Plugins panel to Settings for browsing the plugin marketplace and installing, enabling, disabling, and removing plugins. + +### Patch Changes + +- [#3444](https://github.com/MoonshotAI/kimi-code/pull/3444) [`b4ae7f8`](https://github.com/MoonshotAI/kimi-code/commit/b4ae7f875dddcc40878c8d48d29bce02727dd87c) Thanks [@sailist](https://github.com/sailist)! - Remove the workspace restriction on the Bash tool's cwd parameter. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Added a code wrap toggle to the diff panel and streamlined its header. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed new content not appearing after auto-compaction (stuck at "Requesting…" above the divider); output after the compaction point now renders below the divider. + +- [#3392](https://github.com/MoonshotAI/kimi-code/pull/3392) [`616d510`](https://github.com/MoonshotAI/kimi-code/commit/616d51045dbb7c3949c05713d4a0273c74dd07fc) Thanks [@7Sageer](https://github.com/7Sageer)! - Preserve comments, key order, and formatting in config.toml when configuration values are updated. + +- [#3348](https://github.com/MoonshotAI/kimi-code/pull/3348) [`9d2304c`](https://github.com/MoonshotAI/kimi-code/commit/9d2304c23ca30c781b1a39540971dcaef085a500) Thanks [@liukx0205](https://github.com/liukx0205)! - Fix models and providers transiently disappearing when config.toml is saved non-atomically by an external editor while the daemon reloads it. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: The connecting splash screen now shows the current loading stage and the reason when the connection fails. + +- [#3290](https://github.com/MoonshotAI/kimi-code/pull/3290) [`4b9888b`](https://github.com/MoonshotAI/kimi-code/commit/4b9888b73db5937f86c65d1880c44f5326acd69d) Thanks [@sailist](https://github.com/sailist)! - Block dangerous shell commands such as shutdown, reboot, or rm -rf in Auto mode, and always ask before running them in Manual and YOLO modes; disable the guard with `[permission] dangerous_command_guard = false` or `KIMI_CODE_DANGEROUS_COMMAND_GUARD=false`. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed queued or steered messages being unexpectedly retracted by pressing Esc after they start running. + +- [#3421](https://github.com/MoonshotAI/kimi-code/pull/3421) [`9c37feb`](https://github.com/MoonshotAI/kimi-code/commit/9c37feb473cddb0b8bfe2552ad481f83f83fe6d0) Thanks [@sailist](https://github.com/sailist)! - Make session forks much faster. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show a refresh button in the right-sidebar file preview when the previewed file is edited mid-turn. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed the server-side attachment notice text leaking into your own message bubble after sending a file. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed misaligned content in the composer attachment tooltip. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed messages sent with Ctrl+S while the agent is running disappearing after a page reload. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed sessions occasionally stuck showing "working" after a message is appended mid-turn. + +- [#3377](https://github.com/MoonshotAI/kimi-code/pull/3377) [`58b74cf`](https://github.com/MoonshotAI/kimi-code/commit/58b74cfeab157483eef8a9e4ed8f4b683eecb34d) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix duplicate user messages in transcript clients. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed the first pinyin letter being committed as English text in Chinese IMEs after enabling goal or plan mode. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed sessions occasionally stuck showing "working" long after the reply had completed. + +- [#3427](https://github.com/MoonshotAI/kimi-code/pull/3427) [`442b563`](https://github.com/MoonshotAI/kimi-code/commit/442b56391bd8f1cdc65bb0bab2b6a57c786ec871) Thanks [@sailist](https://github.com/sailist)! - Honor explicit `[experimental]` config entries over the `KIMI_CODE_EXPERIMENTAL_FLAG` master switch, so a flag set to `false` in `config.toml` stays off; per-feature `KIMI_CODE_EXPERIMENTAL_` variables still override both. + +- [#3412](https://github.com/MoonshotAI/kimi-code/pull/3412) [`7bc5b20`](https://github.com/MoonshotAI/kimi-code/commit/7bc5b2027cd80e19dcacf43ed92aad749964a9e3) Thanks [@7Sageer](https://github.com/7Sageer)! - Send the forked-subagent context notice as a system reminder. + +- [#3415](https://github.com/MoonshotAI/kimi-code/pull/3415) [`82bf0a8`](https://github.com/MoonshotAI/kimi-code/commit/82bf0a8dd283da1c25d3eb83c44e310d2bcbdee1) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Parse `git status --porcelain` with `-z` so non-ASCII paths are no longer mangled into bogus quoted directory segments. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Pressing Tab in the @ file menu completes the highlighted candidate's name into the input while keeping the menu open for further filtering. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Support activating multiple skills from a single message. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Background task notification cards now show a "Sent from background · bash" source line and a single "status: task description" body line. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Background task notification cards now label the sender as "Sent from background (Bash) / (Agent)". + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Superseded @ file searches are now cancelled promptly during fast typing, reducing background load. + +- [#3371](https://github.com/MoonshotAI/kimi-code/pull/3371) [`9e88152`](https://github.com/MoonshotAI/kimi-code/commit/9e881528a89945a373002b0b229f91735e8f2c4f) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Fix prompts remaining queued forever after reopening a session. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed manually typed > quote blocks in messages being misrendered as quote annotations; they now render as plain text. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed messages rejected by the server sometimes showing no failure toast. + +- [#3425](https://github.com/MoonshotAI/kimi-code/pull/3425) [`ceb5153`](https://github.com/MoonshotAI/kimi-code/commit/ceb51535efa58d9a9eaa176140ec64959d980c53) Thanks [@sailist](https://github.com/sailist)! - Add the `kimi session list` command to list sessions from the command line. + +- [#3390](https://github.com/MoonshotAI/kimi-code/pull/3390) [`76c1a7a`](https://github.com/MoonshotAI/kimi-code/commit/76c1a7a347ca0bfae68f85d8d4d69d73671c0403) Thanks [@Grapedge](https://github.com/Grapedge)! - Simplify the built-in system prompt. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Selecting a skill from the slash menu now inserts the same skill pill as the @ menu. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed stale steered-message bubbles lingering after a transcript refresh. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed the file-change summary card appearing too early when a turn is steered while still running. + +- [#3436](https://github.com/MoonshotAI/kimi-code/pull/3436) [`0f39b2c`](https://github.com/MoonshotAI/kimi-code/commit/0f39b2cf3aa7b83f7049f922f9babf4b36092ddc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix idle sessions briefly showing a "Working" state when opened in desktop and web clients. + +- [#3346](https://github.com/MoonshotAI/kimi-code/pull/3346) [`ece9618`](https://github.com/MoonshotAI/kimi-code/commit/ece96185e93742db4771de83147f709f22ca6130) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode (experimental, `KIMI_CODE_EXPERIMENTAL_TOWER=1`): spawned workers now start from the base checkout's uncommitted changes instead of missing them, and TowerMerge refuses to merge while the checkout still holds those changes uncommitted. Also, a new session can now enter tower mode after the previous owning session stopped without exiting, instead of being refused while that session stays open. Tower mode now stays on after tower teardown; turn it off explicitly with /tower off. Tower mode is now mutually exclusive with plan mode and swarm mode: entering any one of them exits the others. + +- [#3399](https://github.com/MoonshotAI/kimi-code/pull/3399) [`c3bf6f9`](https://github.com/MoonshotAI/kimi-code/commit/c3bf6f9d2d9d9de53a86052193c038b324eebeca) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode (experimental, `KIMI_CODE_EXPERIMENTAL_TOWER=1`): the agent can no longer enter tower mode on its own — turn it on with /tower on, or with /tower (also in the web UI) to pin the local branch missions merge back into; a missing base branch is created from the current checkout (uncommitted changes committed onto it as a labeled WIP snapshot) and the workspace is initialized or rebased to it immediately, refusing with guidance while missions are open. Tower agents that die (failed, timed out, killed, or lost) are recorded in the tower protocol — TowerStatus marks them in the roster and warns about missions whose owner died, with a resume hint — and the tower's console instructions now require summarizing every worker's deliverables per mission before teardown. + +- [#3454](https://github.com/MoonshotAI/kimi-code/pull/3454) [`913a242`](https://github.com/MoonshotAI/kimi-code/commit/913a24228beaad4bcd0a9c9ee704999b0d617ad4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fixed the context usage ring not refreshing after compaction; session usage now updates live from transcript metadata. + +- [#3391](https://github.com/MoonshotAI/kimi-code/pull/3391) [`5f0aa7f`](https://github.com/MoonshotAI/kimi-code/commit/5f0aa7f6d61c1ce5e26f11852375fc7fd94db27b) Thanks [@7Sageer](https://github.com/7Sageer)! - Default the workspace trust prompt selection to "Trust this folder" instead of "Don't trust". + +- [#3366](https://github.com/MoonshotAI/kimi-code/pull/3366) [`9619277`](https://github.com/MoonshotAI/kimi-code/commit/961927739ef34819d67d76fa5870cbe4ba7a01ff) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Use the Unicode ellipsis "…" in user-facing TUI and VS Code webview text. + +- [#3405](https://github.com/MoonshotAI/kimi-code/pull/3405) [`630a11d`](https://github.com/MoonshotAI/kimi-code/commit/630a11db51ab0ac422cae6a10580b62c1ae8e05f) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Download compressed native update artifacts and decompress them while staging. + +## 0.39.1 + +### Patch Changes + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Fix the command tool row rendering noticeably taller than other tool rows. + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Fix the first IME (or keyboard) character being silently swallowed after clicking the placeholder text in an empty composer. + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Fix switching the permission mode in one session changing it for every session; the permission mode is now scoped per session. + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Fix flickering and broken interactions in the image and video attachment preview popovers. + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Fix attachments in a newly created session still showing as uploading after the upload has finished. + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Render the rich composer placeholder outside the editor, fixing the first typed/IME character being swallowed. + +- [#3307](https://github.com/MoonshotAI/kimi-code/pull/3307) [`0310f22`](https://github.com/MoonshotAI/kimi-code/commit/0310f223daf9596ac403e94c7224ce2f744951c3) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Increase the request timeout for `kimi update`. + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Unify right-side panel headers and give the OpenIn menu a file mode (copy absolute path, editor picker, full-path tooltip). + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Fix signed-in users without a usable model being wrongly asked to sign in (and getting stuck there on web); the send gate now offers picking or configuring a model instead. + +- [#3333](https://github.com/MoonshotAI/kimi-code/pull/3333) [`8f43674`](https://github.com/MoonshotAI/kimi-code/commit/8f43674b902213f876359d82fa3831f485e3e82b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Fix startup getting stuck on "Connecting…" for a long time when the account has many workspaces. + +- [#3328](https://github.com/MoonshotAI/kimi-code/pull/3328) [`dc6028d`](https://github.com/MoonshotAI/kimi-code/commit/dc6028dc6b5c9464039f16cfe38de6ba90a68b72) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fixed skill instructions injected by the Skill tool showing up as ordinary user messages in the rebuilt transcript. + +- [#3292](https://github.com/MoonshotAI/kimi-code/pull/3292) [`23921e9`](https://github.com/MoonshotAI/kimi-code/commit/23921e9f2c5a50f66ad5616554fac8565772919d) Thanks [@7Sageer](https://github.com/7Sageer)! - When a session resumes, the assistant is warned that background tasks from the previous session may still be running. + +## 0.39.0 + +### Minor Changes + +- [#3034](https://github.com/MoonshotAI/kimi-code/pull/3034) [`f0a6094`](https://github.com/MoonshotAI/kimi-code/commit/f0a609487fb835371c608cde101a6ff544c3c33e) Thanks [@sailist](https://github.com/sailist)! - Add Remote Control as an experimental feature for accessing a local web session remotely. Enable it with `KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL=1`, then run `kimi rc`, `kimi web --remote-control`, or `/remote-control` to start it. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the slash-command and @-mention panels failing to open on mobile — both panels and the + menu are now grab-handle bottom sheets on small screens. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add a flat/by-workspace tab to the mobile session list. + +- [#3296](https://github.com/MoonshotAI/kimi-code/pull/3296) [`df9e858`](https://github.com/MoonshotAI/kimi-code/commit/df9e8583882bc0fbc8ff824fc1c627c9bdbc315b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Revamp the right sidebar as a multi-tab panel. + +- [#3007](https://github.com/MoonshotAI/kimi-code/pull/3007) [`f6736d7`](https://github.com/MoonshotAI/kimi-code/commit/f6736d7c0de609d44ed1cb761cfe9f195c4d94fb) Thanks [@7Sageer](https://github.com/7Sageer)! - Add an optional `fork` parameter to subagent and swarm tools that starts the subagent with a snapshot of the calling agent's conversation history; set `KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK=1` or `subagent_fork = true` under `[experimental]` in config.toml to enable it. + +- [#3296](https://github.com/MoonshotAI/kimi-code/pull/3296) [`df9e858`](https://github.com/MoonshotAI/kimi-code/commit/df9e8583882bc0fbc8ff824fc1c627c9bdbc315b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Allow moving a running foreground Bash command or subagent to the background via the "Move to background" button on the running card. + +- [#3099](https://github.com/MoonshotAI/kimi-code/pull/3099) [`0f44537`](https://github.com/MoonshotAI/kimi-code/commit/0f44537c13e7c32b9189e20af7c894c34704be5b) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add experimental tower mode for multi-agent orchestration; set `KIMI_CODE_EXPERIMENTAL_TOWER=1`, then run `/tower on` and `/tower ` to start. + +### Patch Changes + +- [#3241](https://github.com/MoonshotAI/kimi-code/pull/3241) [`1dc34b4`](https://github.com/MoonshotAI/kimi-code/commit/1dc34b46de62a6aa0e308a71d824ecb7487b1374) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Silence the MaxListenersExceededWarning that could appear during long agent turns with many parallel tool calls. + +- [#3166](https://github.com/MoonshotAI/kimi-code/pull/3166) [`d4e0ad4`](https://github.com/MoonshotAI/kimi-code/commit/d4e0ad4b2d04d676b6d139ee320ea162289d3f4b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix thinking blocks in the subagent detail panel being stuck expanded and not collapsible. + +- [#3139](https://github.com/MoonshotAI/kimi-code/pull/3139) [`381142a`](https://github.com/MoonshotAI/kimi-code/commit/381142aff1d165f4bf67327035afec81ab4f656b) Thanks [@sailist](https://github.com/sailist)! - Fix sessions failing to archive when their workspace folder no longer exists. + +- [#3034](https://github.com/MoonshotAI/kimi-code/pull/3034) [`f0a6094`](https://github.com/MoonshotAI/kimi-code/commit/f0a609487fb835371c608cde101a6ff544c3c33e) Thanks [@sailist](https://github.com/sailist)! - Fix messages sent from one web client not appearing on other clients connected to the same session. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix pressing Esc to cancel an IME candidate also closing the BTW side chat. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the composer not receiving focus after opening the BTW side chat via the shortcut or /btw. + +- [#3212](https://github.com/MoonshotAI/kimi-code/pull/3212) [`a664226`](https://github.com/MoonshotAI/kimi-code/commit/a664226bf2244a232fd778064e2f1edf7691d268) Thanks [@sailist](https://github.com/sailist)! - Preserve the active session and its selected model when logging out of a provider. + +- [#3136](https://github.com/MoonshotAI/kimi-code/pull/3136) [`e9a99e5`](https://github.com/MoonshotAI/kimi-code/commit/e9a99e5ec6843b590c44c63c3d604702c24b1bca) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add the Tencent CloudBase plugin to the curated marketplace. + +- [#3296](https://github.com/MoonshotAI/kimi-code/pull/3296) [`df9e858`](https://github.com/MoonshotAI/kimi-code/commit/df9e8583882bc0fbc8ff824fc1c627c9bdbc315b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improve code block interaction and rendering. + +- [#3296](https://github.com/MoonshotAI/kimi-code/pull/3296) [`df9e858`](https://github.com/MoonshotAI/kimi-code/commit/df9e8583882bc0fbc8ff824fc1c627c9bdbc315b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improve composer interaction, including the presentation of file, folder, and media attachments. + +- [#3152](https://github.com/MoonshotAI/kimi-code/pull/3152) [`3090c1c`](https://github.com/MoonshotAI/kimi-code/commit/3090c1c4821df5e901c8d92dc9b77341fa16747a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix composer toolbar buttons squeezing and overlapping each other in very narrow windows. + +- [#3154](https://github.com/MoonshotAI/kimi-code/pull/3154) [`54bb49e`](https://github.com/MoonshotAI/kimi-code/commit/54bb49e138ad8a2c6e20008b4ea32a3917cc7b1a) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix the latest reply disappearing from the transcript after a scheduled cron reminder fires. + +- [#3034](https://github.com/MoonshotAI/kimi-code/pull/3034) [`f0a6094`](https://github.com/MoonshotAI/kimi-code/commit/f0a609487fb835371c608cde101a6ff544c3c33e) Thanks [@sailist](https://github.com/sailist)! - Remove the `--allow-remote-terminals` flag from `kimi web`; PTY terminal routes now stay available on loopback binds only. + +- [#3183](https://github.com/MoonshotAI/kimi-code/pull/3183) [`2adc6a1`](https://github.com/MoonshotAI/kimi-code/commit/2adc6a1c6e1adeb696b0edc00a77ef90c54c8218) Thanks [@sailist](https://github.com/sailist)! - Fix ACP session regressions: Bash, Grep, and Glob failing when the editor does not support terminal command execution, session creation failing with stdio MCP servers, and reopening a closed session failing with an internal error. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix memory usage growing steadily after repeatedly switching sessions and toggling the side chat and subagent panels. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix unsent composer attachments such as images being lost after switching sessions on the new-session page. + +- [#3296](https://github.com/MoonshotAI/kimi-code/pull/3296) [`df9e858`](https://github.com/MoonshotAI/kimi-code/commit/df9e8583882bc0fbc8ff824fc1c627c9bdbc315b) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix known issues. + +- [#3294](https://github.com/MoonshotAI/kimi-code/pull/3294) [`21f7ef6`](https://github.com/MoonshotAI/kimi-code/commit/21f7ef64f0851504227617f4501bf8359031d9a5) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix sign-in briefly showing a device-code-expired error after a successful authorization. + +- [#3152](https://github.com/MoonshotAI/kimi-code/pull/3152) [`3090c1c`](https://github.com/MoonshotAI/kimi-code/commit/3090c1c4821df5e901c8d92dc9b77341fa16747a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix long question text in question cards being truncated with an ellipsis instead of wrapping. + +- [#3206](https://github.com/MoonshotAI/kimi-code/pull/3206) [`4d5147b`](https://github.com/MoonshotAI/kimi-code/commit/4d5147ba5da6267f52b10416469f275138fa51ce) Thanks [@sailist](https://github.com/sailist)! - Fix repeated server crashes when resuming a session that was interrupted in the middle of a turn. + +- [#3159](https://github.com/MoonshotAI/kimi-code/pull/3159) [`ea0626a`](https://github.com/MoonshotAI/kimi-code/commit/ea0626ad48ee318045a22490d52c86be7d086033) Thanks [@pvzheroes125](https://github.com/pvzheroes125)! - Prevent AskUserQuestion from starting background tasks when task controls are unavailable. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Remove the redundant parenthesized domain from the login entry card titles. + +- [#3234](https://github.com/MoonshotAI/kimi-code/pull/3234) [`74d9bd1`](https://github.com/MoonshotAI/kimi-code/commit/74d9bd132e0b056e7d40235070b94bd7d3f4d5f2) Thanks [@xpzouying](https://github.com/xpzouying)! - Send MCP structuredContent to the model only when the tool result has no usable content, avoiding duplicate tool output. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the send and stop button icons rendering too small in the mobile composer. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Present the mobile model picker as a bottom sheet consistent with the other mobile drawers. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the oversized appearance theme cards in the mobile first-run wizard. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Temporarily remove the custom-provider entry from the mobile first-run wizard. + +- [#3152](https://github.com/MoonshotAI/kimi-code/pull/3152) [`3090c1c`](https://github.com/MoonshotAI/kimi-code/commit/3090c1c4821df5e901c8d92dc9b77341fa16747a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improve mobile UI styling. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix tool-call rows alternating heights on mobile by unifying them to the compact row height. + +- [#3152](https://github.com/MoonshotAI/kimi-code/pull/3152) [`3090c1c`](https://github.com/MoonshotAI/kimi-code/commit/3090c1c4821df5e901c8d92dc9b77341fa16747a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Collapse the composer model picker to an icon when space is tight; hovering still shows the model and reasoning effort. + +- [#3152](https://github.com/MoonshotAI/kimi-code/pull/3152) [`3090c1c`](https://github.com/MoonshotAI/kimi-code/commit/3090c1c4821df5e901c8d92dc9b77341fa16747a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the composer permission mode label being hidden even when there is enough space. + +- [#3219](https://github.com/MoonshotAI/kimi-code/pull/3219) [`d1a46db`](https://github.com/MoonshotAI/kimi-code/commit/d1a46db94efe5ed74ad2b665abdb8d697723b81f) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Show the /plugins marketplace catalog as soon as it loads, with latest-version lookups running in the background. + +- [#3002](https://github.com/MoonshotAI/kimi-code/pull/3002) [`d723cc4`](https://github.com/MoonshotAI/kimi-code/commit/d723cc47ee43e5ca3c3c4ec2473f205d44acede2) Thanks [@7Sageer](https://github.com/7Sageer)! - Respect workspace trust and configuration readiness when managing MCP servers. + +- [#3191](https://github.com/MoonshotAI/kimi-code/pull/3191) [`ee53d84`](https://github.com/MoonshotAI/kimi-code/commit/ee53d84fb0d0c1aa023e219b640dfa8faf6c0d38) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix subagents bound to a configured secondary model ignoring its default thinking effort. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix mismatched left and right margins in the sidebar session list, and show the scrollbar only while hovering or scrolling. + +- [#3164](https://github.com/MoonshotAI/kimi-code/pull/3164) [`41a75ad`](https://github.com/MoonshotAI/kimi-code/commit/41a75adfc7a56c2006c93c0b6089cf4457bce20d) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fix the context usage bar in /usage and the footer showing a stale percentage after the context size or model changes. + +- [#3198](https://github.com/MoonshotAI/kimi-code/pull/3198) [`496bb6c`](https://github.com/MoonshotAI/kimi-code/commit/496bb6ce4e555c11304074c31312c01edf4d773a) Thanks [@sailist](https://github.com/sailist)! - Add a dedicated `[swarm] timeout_ms` config option (or the `KIMI_CODE_SWARM_TIMEOUT_MS` env var) for AgentSwarm subagent timeouts, which no longer follow `[subagent] timeout_ms`. + +- [#3152](https://github.com/MoonshotAI/kimi-code/pull/3152) [`3090c1c`](https://github.com/MoonshotAI/kimi-code/commit/3090c1c4821df5e901c8d92dc9b77341fa16747a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Restyle background task notifications as a lighter notice that shows the task summary, output files, and output preview directly. + +- [#3239](https://github.com/MoonshotAI/kimi-code/pull/3239) [`6595955`](https://github.com/MoonshotAI/kimi-code/commit/6595955b31a6d03fa5ea702141c7e2c0f00ba050) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix foreground subagents being reported as background tasks on the task list. + +- [#3227](https://github.com/MoonshotAI/kimi-code/pull/3227) [`4b04492`](https://github.com/MoonshotAI/kimi-code/commit/4b044926e3ca4bc98916128a9fb4ce2b2906cc4f) Thanks [@7Sageer](https://github.com/7Sageer)! - Save oversized tool output within safety limits for later inspection, report omitted MCP content, and retain partial assistant responses when streams fail. + +- [#3102](https://github.com/MoonshotAI/kimi-code/pull/3102) [`2f12469`](https://github.com/MoonshotAI/kimi-code/commit/2f124693017b100346ae2a4928e7bf67dc679ddb) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix the cold transcript rebuild splitting a turn at background-task completion notices; they now fold into the current turn like the live stream does. + +- [#3271](https://github.com/MoonshotAI/kimi-code/pull/3271) [`75c94c4`](https://github.com/MoonshotAI/kimi-code/commit/75c94c4a0e87be084a14096dee2118dfa420a4af) Thanks [@sailist](https://github.com/sailist)! - Fix attached images disappearing from the user message while the agent is working. + +- [#3278](https://github.com/MoonshotAI/kimi-code/pull/3278) [`b17bd61`](https://github.com/MoonshotAI/kimi-code/commit/b17bd61cefba3ea0aef5c61d5cd1085c8cfde065) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fix the "manually stopped" state lingering after undoing the interrupted turn. + +- [#3157](https://github.com/MoonshotAI/kimi-code/pull/3157) [`491ebd0`](https://github.com/MoonshotAI/kimi-code/commit/491ebd050f421de231fc4c91cdb51f7c100db649) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the reset-time hint in the sidebar usage panel being ellipsized even when there is enough room. + +- [#2200](https://github.com/MoonshotAI/kimi-code/pull/2200) [`30e7f62`](https://github.com/MoonshotAI/kimi-code/commit/30e7f62d2c2c2fdaef785c544a47d0ade3e9788f) Thanks [@wszqkzqk](https://github.com/wszqkzqk)! - Fix file tools and shell working directories failing to resolve Git Bash paths such as /c/Users or /tmp on Windows. + +- [#3281](https://github.com/MoonshotAI/kimi-code/pull/3281) [`7de7b18`](https://github.com/MoonshotAI/kimi-code/commit/7de7b18ee95e65d6627f884194d7dc97d77114a2) Thanks [@sailist](https://github.com/sailist)! - Fix sessions failing to resume when their session journal was truncated or corrupted, for example after a full disk. + +## 0.38.0 + +### Minor Changes + +- [#2862](https://github.com/MoonshotAI/kimi-code/pull/2862) [`3d77620`](https://github.com/MoonshotAI/kimi-code/commit/3d7762003a4a35cbeb8571d471c6898a006152e6) Thanks [@liruifengv](https://github.com/liruifengv)! - Support two OAuth login methods — kimi.ai and kimi.com. + +- [#3060](https://github.com/MoonshotAI/kimi-code/pull/3060) [`8440801`](https://github.com/MoonshotAI/kimi-code/commit/8440801de47ddae29224430048e1228b80cde370) Thanks [@chengluyu](https://github.com/chengluyu)! - Add the WaitFor tool: the agent can now wait for a background task to finish within the current turn instead of ending the turn and being re-invoked. + +### Patch Changes + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Label inline subagent cards in the message stream with their foreground or background mode. + +- [#3121](https://github.com/MoonshotAI/kimi-code/pull/3121) [`3899079`](https://github.com/MoonshotAI/kimi-code/commit/3899079a2c851bd0b3f1cbf1d3d2fd9026fc6abb) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix config.toml entries being lost when the file had a syntax error or was edited outside the app. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add copy buttons next to the server version and server address in settings. + +- [#3119](https://github.com/MoonshotAI/kimi-code/pull/3119) [`a34d02a`](https://github.com/MoonshotAI/kimi-code/commit/a34d02a64f9b1526ec84e161d8c377654b413624) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add 13 data sources to the official Kimi Datasource plugin — Chinese government data (NDA/NBS) and standards (GB/HB/DB/TT), eight international organization datasets (WHO, FAO, UNSD, ECB, Eurostat, UNICEF, OECD, FRED), Xinhua Finance, and Caixin. Update the plugin from the Official tab in /plugins. + +- [#3096](https://github.com/MoonshotAI/kimi-code/pull/3096) [`67fbcdf`](https://github.com/MoonshotAI/kimi-code/commit/67fbcdf1ba7dceeebb58875b3b7c81b4b30cf0de) Thanks [@sailist](https://github.com/sailist)! - Edit and Write now require reading an existing file before modifying it, and reject the write when the file changed on disk since it was last read. + +- [#3101](https://github.com/MoonshotAI/kimi-code/pull/3101) [`d96b4a0`](https://github.com/MoonshotAI/kimi-code/commit/d96b4a0149f3ddf3d4910cc6eb87366dbb130ede) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Stop retrying requests blocked by the provider content filter; the filter notice now shows immediately. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Keep empty workspace groups visible in the legacy sidebar after their last session is archived. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Hide button hover tooltips outside a menu while the menu is open. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Keep the model picker menu on the workspace home within the viewport. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the workspace group title showing untranslated text in the search dialog. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the settings dialog dropdown list being clipped by the scroll area, and lock the content behind it while the list is open. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Keep the slash command and @ mention panels on the workspace home within the viewport. + +- [#3052](https://github.com/MoonshotAI/kimi-code/pull/3052) [`6595a69`](https://github.com/MoonshotAI/kimi-code/commit/6595a6989a68163e10a85c8edf1726b30d6d2c2b) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix 422 errors from some OpenAI-compatible providers when a conversation includes tool calls. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Prevent text selection in the sidebar user menu and its plan usage submenu. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix slow session list loading when there are many workspaces. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Auto-open the browser authorization page after choosing a login region, redesign the authorization waiting page, and refresh the login state as soon as the window regains focus instead of waiting for the poll. + +- [#3083](https://github.com/MoonshotAI/kimi-code/pull/3083) [`571bcc2`](https://github.com/MoonshotAI/kimi-code/commit/571bcc2f751f02a37b0475b074a1e859c7fc4368) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix the missing OAuth authenticate tool for remote MCP servers that require login. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Upgrade the @ mention menu: file and skill candidates are merged and ranked by match quality, file search is faster, with path-fragment matching and hit highlighting. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Round menu items concentric with their menu frames. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add a Pin action to the chat header more-menu to pin the current session to the sidebar pinned section. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Allow dragging the divider between the pinned section and the session list to resize both areas, with fade hints at the edges when the pinned section scrolls. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improve the prompt queue interaction, with per-row steer and send. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add kimi.com and kimi.ai OAuth login entries, and switch update and help links to the site matching the current login. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Remove sessions archived from another client from the session list immediately, without a manual refresh. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Label the timestamp at the bottom of the session menu as last active and tighten that row's padding. + +- [#3054](https://github.com/MoonshotAI/kimi-code/pull/3054) [`cfc3350`](https://github.com/MoonshotAI/kimi-code/commit/cfc335048378d3708666e11959c8d34507a1d659) Thanks [@Grapedge](https://github.com/Grapedge)! - Collapse long `!` shell command output instead of flooding the transcript. Press ctrl+o to expand or collapse it together with tool output. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix misaligned action buttons between the sidebar section headers and the session rows. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Remove the skill-activated card from skill activation messages. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Make skill-activation turns undoable so they can be withdrawn and resent. + +- [#3012](https://github.com/MoonshotAI/kimi-code/pull/3012) [`ca87c58`](https://github.com/MoonshotAI/kimi-code/commit/ca87c58e6205ddf0638e5d737a5f8e939e2132b9) Thanks [@sailist](https://github.com/sailist)! - Sub-agents no longer spawn their own sub-agents by default; custom agent profiles can still allow it explicitly. + +- [#3005](https://github.com/MoonshotAI/kimi-code/pull/3005) [`be8e017`](https://github.com/MoonshotAI/kimi-code/commit/be8e017597b83142282d7e6640076368bf244eae) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix background agent rows that could not be stopped right after they appeared, and stray rows left behind when an agent failed to start. + +- [#3046](https://github.com/MoonshotAI/kimi-code/pull/3046) [`f13f379`](https://github.com/MoonshotAI/kimi-code/commit/f13f3790448f64448c76a415500041443ae754e6) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix the model being directed to unavailable tools when it encounters an image or binary file. + +- [#3108](https://github.com/MoonshotAI/kimi-code/pull/3108) [`05f2ad5`](https://github.com/MoonshotAI/kimi-code/commit/05f2ad5ddad1addf10ead6f5274554ca10cde1f4) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: clearing a goal now removes it from the transcript view instead of leaving the stale goal displayed. + +- [#3108](https://github.com/MoonshotAI/kimi-code/pull/3108) [`05f2ad5`](https://github.com/MoonshotAI/kimi-code/commit/05f2ad5ddad1addf10ead6f5274554ca10cde1f4) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: attachments sent with a prompt now appear in the live transcript immediately instead of only after a reload. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Tighten the row height and spacing of the account menu and its submenus to match the standard menu density. + +- [#3135](https://github.com/MoonshotAI/kimi-code/pull/3135) [`2c5415f`](https://github.com/MoonshotAI/kimi-code/commit/2c5415f930db3edef3e79a4e88c4ee74af123600) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Give WaitFor tool calls a dedicated quiet-line display showing completed tasks, wait timeouts, and how many tasks are still running. + +## 0.37.2 + +### Patch Changes + +- [#3061](https://github.com/MoonshotAI/kimi-code/pull/3061) [`5c661f4`](https://github.com/MoonshotAI/kimi-code/commit/5c661f4610f36481dbf2f9598aa63f49004e4980) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: the subagent detail panel now keeps the working process fully expanded and drops the end-of-turn timestamp footer. + +- [#3061](https://github.com/MoonshotAI/kimi-code/pull/3061) [`5c661f4`](https://github.com/MoonshotAI/kimi-code/commit/5c661f4610f36481dbf2f9598aa63f49004e4980) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Settings gains a Lab tab with a multi-tab sidebar toggle (off by default); when enabled, the sidebar shows the Open / Done / Workspaces tabs. + +## 0.37.1 + +### Patch Changes + +- [#3053](https://github.com/MoonshotAI/kimi-code/pull/3053) [`95cede8`](https://github.com/MoonshotAI/kimi-code/commit/95cede82b4d3b6cb1845c66e87896ab2e5fd9ba5) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix pasted images failing to reach the model on first send. + +- [#3047](https://github.com/MoonshotAI/kimi-code/pull/3047) [`c9c34ae`](https://github.com/MoonshotAI/kimi-code/commit/c9c34ae5a8626f133bd1b9c34cac0f3270e35b8d) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix pasted videos failing to submit instead of reaching the model. + +## 0.37.0 + +### Minor Changes + +- [#2935](https://github.com/MoonshotAI/kimi-code/pull/2935) [`44a6c70`](https://github.com/MoonshotAI/kimi-code/commit/44a6c70e66762ea9e122f8dceae16dc759086a7c) Thanks [@chengluyu](https://github.com/chengluyu)! - Activate multiple skills in a single prompt. Type `/` after whitespace to insert a skill token. + +- [#2994](https://github.com/MoonshotAI/kimi-code/pull/2994) [`8c865f4`](https://github.com/MoonshotAI/kimi-code/commit/8c865f48173011439cfc2e140e45586e59b6bfcf) Thanks [@liruifengv](https://github.com/liruifengv)! - The Windows native (single-binary) CLI now supports automatic updates. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: the sidebar gains Open / Done / Workspaces tabs, and sessions can be marked as done (and reopened) to keep the open list focused. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: added a session management page (from the sidebar's list-management menu) for cross-workspace triage — filter by workspace, status, and updated time, and batch mark sessions as done or reopen them. + +### Patch Changes + +- [#2593](https://github.com/MoonshotAI/kimi-code/pull/2593) [`d833a1a`](https://github.com/MoonshotAI/kimi-code/commit/d833a1a893c4d69d96af542f40557442992085e0) Thanks [@7Sageer](https://github.com/7Sageer)! - Keep pasted image and video attachments available in session history. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: @-mentioned files, folders, and skills in chat messages now render as icon pills. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: renamed the Subagent panel to "Background Agent". + +- [#2914](https://github.com/MoonshotAI/kimi-code/pull/2914) [`1cf617d`](https://github.com/MoonshotAI/kimi-code/commit/1cf617d769a887f5d8306ebc16a1e078b5e47049) Thanks [@SeleneXX](https://github.com/SeleneXX)! - Fix Gemini tool-calling sessions failing on follow-up requests. + +- [#2972](https://github.com/MoonshotAI/kimi-code/pull/2972) [`04d23e2`](https://github.com/MoonshotAI/kimi-code/commit/04d23e2dab776c480d24cfa033c9500543c75a3b) Thanks [@sailist](https://github.com/sailist)! - Fix text files containing Chinese or emoji being misdetected as binary in the web UI. + +- [#2940](https://github.com/MoonshotAI/kimi-code/pull/2940) [`6b72345`](https://github.com/MoonshotAI/kimi-code/commit/6b72345f8bb03487e3bcc05b541e65484818428c) Thanks [@bj456736](https://github.com/bj456736)! - Print and copy the full `kimi --resume` command after `/fork`. + +- [#2928](https://github.com/MoonshotAI/kimi-code/pull/2928) [`d96cd03`](https://github.com/MoonshotAI/kimi-code/commit/d96cd037702637305422222e985139e51ff83c8c) Thanks [@chengluyu](https://github.com/chengluyu)! - Warn when a typed `/goal` objective exceeds the 4000-character limit, and keep the input if it is rejected. + +- [#2633](https://github.com/MoonshotAI/kimi-code/pull/2633) [`f492cd7`](https://github.com/MoonshotAI/kimi-code/commit/f492cd7c9e03666ecfd10dc47ca9b48c35de2318) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Fix slow startup by loading the global search index on demand. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed YAML frontmatter in messages rendering as a giant heading — it now shows as a small meta block. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed plain text like "(c)", "(tm)", and "--" in messages being rewritten as ©, ™, and dashes — message text now renders verbatim. + +- [#2985](https://github.com/MoonshotAI/kimi-code/pull/2985) [`a7dc1ea`](https://github.com/MoonshotAI/kimi-code/commit/a7dc1ea28445555d5944066936fdf6e1b21d27ea) Thanks [@bj456736](https://github.com/bj456736)! - Fix a startup error when a restored session references a model that is no longer configured. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: hovering a mention pill now shows a detail bubble (full path for files and folders, description plus an open button for skills), skill and file mentions in messages are clickable, long file names middle-ellipsize, and deleted files are struck through. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed long task panel titles pushing the status badge, copy, and close buttons out of view — titles now ellipsize and show the full text on hover. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed pasting a copied folder into the composer failing the upload with a connection error — folders are now skipped instead. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: reduced animation power draw — the mascot and home doodle pause while hidden or scrolled offscreen, and looping animations play once and stop when the system's "reduce motion" setting is on. + +- [#2969](https://github.com/MoonshotAI/kimi-code/pull/2969) [`ee564e5`](https://github.com/MoonshotAI/kimi-code/commit/ee564e5ec90afd068123b8052928c53f1fd5a27d) Thanks [@sailist](https://github.com/sailist)! - Fix the displayed context size dropping to a smaller estimate after archiving and resuming a session. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: the plan review feedback box now auto-grows with its content, so longer rejection reasons are easier to write. + +- [#2633](https://github.com/MoonshotAI/kimi-code/pull/2633) [`f492cd7`](https://github.com/MoonshotAI/kimi-code/commit/f492cd7c9e03666ecfd10dc47ca9b48c35de2318) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Queue slash skill commands entered while the agent is busy instead of rejecting them. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: the search dialog now finds workspaces too, and picking a workspace or session result expands the sidebar and scrolls the item into view. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed sent image and video attachments rendering broken in session history after a refresh or reopen. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed empty replies left by manually stopped answers still showing a completion time after reloading the page. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed background agent tasks not being cancellable during their first moments after starting. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed foreground subagents leaking into the Background Agent panel, which broke the count and left finished rows stuck as running and unstoppable. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: merged the task panel's two copy icons into a single button with a dropdown menu (copy command / copy output / copy all), with keyboard and touch-friendly targets. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed cancelled or abnormally ended background tasks showing as completed. + +- [#3016](https://github.com/MoonshotAI/kimi-code/pull/3016) [`98ebda8`](https://github.com/MoonshotAI/kimi-code/commit/98ebda840a1e420f57a05ec680cbeca41a2419d7) Thanks [@sailist](https://github.com/sailist)! - Fix /undo not restoring the todo list to its state before the undone turn. + +- [#2858](https://github.com/MoonshotAI/kimi-code/pull/2858) [`59dde73`](https://github.com/MoonshotAI/kimi-code/commit/59dde734f37596db5c77794060f81bfb3c1dbeb6) Thanks [@7Sageer](https://github.com/7Sageer)! - On the legacy engine, plugin MCP server changes and OAuth sign-in now take effect in open sessions immediately. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: the browser tab title now shows the current workspace directory name (override with the new `--web-title` flag), making instances on multiple machines easier to tell apart. + +- [#3043](https://github.com/MoonshotAI/kimi-code/pull/3043) [`e31b3a3`](https://github.com/MoonshotAI/kimi-code/commit/e31b3a335ed49139c1aa3abb6b45a244fa17356a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fixed Ctrl+K in the composer opening session search on macOS instead of deleting to end of line — session search now only answers to Cmd+K. + +- [#2989](https://github.com/MoonshotAI/kimi-code/pull/2989) [`09976b0`](https://github.com/MoonshotAI/kimi-code/commit/09976b09140c412f81a38cc00191f88bee4a9437) Thanks [@bj456736](https://github.com/bj456736)! - Add `kimi web --web-title ` to set a custom browser tab title for the web UI. + +## 0.36.1 + +### Patch Changes + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: The timestamp under assistant replies now shows the message time instead of the work duration. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Restyle the slash command and @ file mention menus: matched fragments are bold-highlighted in the slash menu, and long lists in both menus get a scroll fade and a draggable floating scrollbar. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: The background Bash panel now supports filtering by status, and clicking a task shows its command and output on the right. + +- [#2865](https://github.com/MoonshotAI/kimi-code/pull/2865) [`53909d9`](https://github.com/MoonshotAI/kimi-code/commit/53909d91e3ca570d4b565ba1abd00f027ca78d6b) Thanks [@weivwang](https://github.com/weivwang)! - Cache content-hashed Kimi Web assets across reloads while keeping the app entry point revalidated. + +- [#2916](https://github.com/MoonshotAI/kimi-code/pull/2916) [`7475c2e`](https://github.com/MoonshotAI/kimi-code/commit/7475c2e2e3dd86ac0b8a8d51d4f1d233ed7df797) Thanks [@Grapedge](https://github.com/Grapedge)! - Cancel an in-flight /init run together with the turn instead of letting it run to completion. + +- [#2911](https://github.com/MoonshotAI/kimi-code/pull/2911) [`249d8fa`](https://github.com/MoonshotAI/kimi-code/commit/249d8faa3447427665185a900926d048213d2ac7) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix sessions hanging on the second approval prompt and tool call results being dropped or mixed up in history when using a self-hosted OpenAI-compatible endpoint that renumbers tool call ids on every response. + +- [#2917](https://github.com/MoonshotAI/kimi-code/pull/2917) [`6cf315b`](https://github.com/MoonshotAI/kimi-code/commit/6cf315b7bdea8a04cfaeba1bb8931c1730853aec) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix bare URLs in chat output absorbing the CJK characters that follow them, which made the link unclickable or open a broken address. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the slash command panel staying open after switching sessions or when the composer loses focus. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Replace the composer mode menu with mutually exclusive plan/goal pills on the left of the input area (arm via /plan or /goal, exit with ×); Swarm becomes a separate toolbar toggle. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Restyle the work status pills above the composer with a borderless rounded look. + +- [#2910](https://github.com/MoonshotAI/kimi-code/pull/2910) [`eb72aeb`](https://github.com/MoonshotAI/kimi-code/commit/eb72aebeeb972b2fcc238d5650dd991a5580f96b) Thanks [@sailist](https://github.com/sailist)! - Remove the 64 MiB limit on web session exports, so large sessions no longer fail with a file-too-large error when downloaded from the web UI. + +- [#2884](https://github.com/MoonshotAI/kimi-code/pull/2884) [`1811bd4`](https://github.com/MoonshotAI/kimi-code/commit/1811bd4baf5b75ba076e2a24825f9c4f82c13341) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix startup banner text wrapping on narrow terminals. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix `$` content inside inline code spans being misrendered as inline math. + +- [#2899](https://github.com/MoonshotAI/kimi-code/pull/2899) [`102984a`](https://github.com/MoonshotAI/kimi-code/commit/102984aa660d752ba8dd7d1aba155575f32affe2) Thanks [@oocz](https://github.com/oocz)! - Fix MCP OAuth cancellation leaving an in-flight authorization waiting for its callback timeout. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the thinking-effort flyout being unreachable when selecting the last model in the subagent model list. + +- [#2876](https://github.com/MoonshotAI/kimi-code/pull/2876) [`5912d4c`](https://github.com/MoonshotAI/kimi-code/commit/5912d4c7d19d68975e85b007976b1bef59edae5c) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix repeated file-watcher errors on Windows when the workspace is a drive root (such as `E:\`) or a UNC network share. + +- [#2916](https://github.com/MoonshotAI/kimi-code/pull/2916) [`7475c2e`](https://github.com/MoonshotAI/kimi-code/commit/7475c2e2e3dd86ac0b8a8d51d4f1d233ed7df797) Thanks [@Grapedge](https://github.com/Grapedge)! - Show a clear error when forking a session while its turn is running, instead of copying a partially written turn. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix forking sessions with very long histories always failing with a timeout. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Selecting /goal from the slash menu now immediately arms a removable goal pill in the composer; typing and sending creates the goal without requiring the goal text after the command. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Restyle the goal panel: the goal text and elapsed time move to the header, and actions become icon buttons. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix CJK text immediately after a bare URL being swallowed into the link, which made the link unopenable. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Adjust when plan mode takes effect: enabling it now arms a removable plan pill in the composer and only activates when the message is sent, matching goal mode behavior. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add a plan viewer panel: click a plan entry in the work bar to see the full plan, review results, and feedback. + +- [#2863](https://github.com/MoonshotAI/kimi-code/pull/2863) [`245e3d5`](https://github.com/MoonshotAI/kimi-code/commit/245e3d56a6de45e74d55449ef26cd65304a3250a) Thanks [@LouisDM](https://github.com/LouisDM)! - Prevent background task output from disrupting terminal pane borders. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show a clear top-center confirmation toast after exporting a session, and a clearer error message when the export fails because the session is too large. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the session list PR badge not refreshing after a PR is created from within a session. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Restyle the session list PR badge as a small tag with a background. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Unify session status display in the sidebar and stabilize session list ordering. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Slash commands now support fuzzy search: find commands by description text, pinyin, or pinyin initials. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Rework the subagent panel into a card grid layout with status filtering, showing in-progress and recently finished tasks by default. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix server request timeouts being misreported as "cannot connect to the Kimi server". + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Restyle the todo panel as frosted cards and add a current-progress completion count. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Increase the font size and row height of the user menu and the plan usage flyout. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Rename the user menu's "Upgrade" entry to "Upgrade membership" and label the plan usage percentage as used. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add experimental automatic session title generation, with on-demand regeneration from the session list. + +- [#2922](https://github.com/MoonshotAI/kimi-code/pull/2922) [`cd48995`](https://github.com/MoonshotAI/kimi-code/commit/cd489955bb42c4a8055de9545d0b699b93bba98a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add a "sort by recent activity" option to the workspace-grouped sidebar view (switched in the view options menu); newly added workspaces now sort to the top. + +## 0.36.0 + +### Minor Changes + +- [#2830](https://github.com/MoonshotAI/kimi-code/pull/2830) [`ec84a6f`](https://github.com/MoonshotAI/kimi-code/commit/ec84a6f9a3eb35e1118f8a327f7a11b3978a899c) Thanks [@liruifengv](https://github.com/liruifengv)! - Add an experimental fullscreen TUI mode. Set the `KIMI_CODE_TUI_FULL_SCREEN=1` environment variable to enable it. + +- [#2700](https://github.com/MoonshotAI/kimi-code/pull/2700) [`c9bfe8b`](https://github.com/MoonshotAI/kimi-code/commit/c9bfe8b2c8314ba4ef8806fb3b92ac654c1d1860) Thanks [@7Sageer](https://github.com/7Sageer)! - Add a configurable model pool for spawned subagents behind the `secondary-model` experiment (`KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1`, or the master flag): with the experiment on, the `/secondary-model` command or the `[secondary_model]` section in config.toml sets a default model or a small named pool that the main agent picks from per spawn. A lone legacy `model` key in the same section keeps working as the fallback default. + +### Patch Changes + +- [#2830](https://github.com/MoonshotAI/kimi-code/pull/2830) [`ec84a6f`](https://github.com/MoonshotAI/kimi-code/commit/ec84a6f9a3eb35e1118f8a327f7a11b3978a899c) Thanks [@liruifengv](https://github.com/liruifengv)! - Render LaTeX math formulas (`$…$` / `$$…$$`) in messages as Unicode formulas. + +- [#2855](https://github.com/MoonshotAI/kimi-code/pull/2855) [`30f56a2`](https://github.com/MoonshotAI/kimi-code/commit/30f56a2d2da332cbf0c36a13cbe01aac5d319c7b) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix Ctrl+C being ignored during automatic retries of failed API requests. + +- [#2819](https://github.com/MoonshotAI/kimi-code/pull/2819) [`fe3cdae`](https://github.com/MoonshotAI/kimi-code/commit/fe3cdae5f8ab40be71b65eff32319eb94a53c17d) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix sessions failing with a provider 400 error on every follow-up request after a turn is interrupted while the model is still thinking, on strict OpenAI-compatible providers. + +- [#2847](https://github.com/MoonshotAI/kimi-code/pull/2847) [`3b0936d`](https://github.com/MoonshotAI/kimi-code/commit/3b0936d8e025c5a944759c40593d5f21bfb3e621) Thanks [@sailist](https://github.com/sailist)! - Fix plain Markdown files (such as CHANGELOG.md) in an installed plugin's root directory being misidentified as skills when the plugin relies on the root SKILL.md fallback. + +- [#2843](https://github.com/MoonshotAI/kimi-code/pull/2843) [`c212ae9`](https://github.com/MoonshotAI/kimi-code/commit/c212ae9715371c0d7939c15e664acbe0d7cf7fc3) Thanks [@sailist](https://github.com/sailist)! - Show project MCP launch targets in the workspace trust prompt, default to declining trust, and resolve fd and stty binaries to absolute paths so untrusted workspaces cannot plant bare-name executables before confirmation. + + `@moonshot-ai/kimi-code-sdk` contract change: `WorkspaceTrustInfo.gatedMcpServers` now carries structured `WorkspaceTrustMcpServerInfo` records (`name`, `transport`, and `command`/`args`/`cwd` or `url`) instead of plain strings, so SDK consumers rendering a trust prompt can show the full launch target. + +- [#2856](https://github.com/MoonshotAI/kimi-code/pull/2856) [`504e629`](https://github.com/MoonshotAI/kimi-code/commit/504e6292ede448367d1341751f9f98b24cc2994f) Thanks [@pvzheroes125](https://github.com/pvzheroes125)! - Refresh active MCP connections after OAuth credentials are added or reset. + +## 0.35.0 + +### Minor Changes + +- [#2816](https://github.com/MoonshotAI/kimi-code/pull/2816) [`ad12ad8`](https://github.com/MoonshotAI/kimi-code/commit/ad12ad8a140d24051d93ec98a4a6921ab33723ff) Thanks [@liruifengv](https://github.com/liruifengv)! - Show the live work progress of background subagents in the `/tasks` panel. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Image and video tool results now open in a fullscreen preview on click, with zoom support for images. + +### Patch Changes + +- [#2731](https://github.com/MoonshotAI/kimi-code/pull/2731) [`437a1b8`](https://github.com/MoonshotAI/kimi-code/commit/437a1b8ba1b7e0f6662bdadc669564fdc58c3f5a) Thanks [@pvzheroes125](https://github.com/pvzheroes125)! - Detect MCP servers that require OAuth without needing `auth: "oauth"` in the config. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Reduce UI stutter while AI responses stream in long sessions. + +- [#2699](https://github.com/MoonshotAI/kimi-code/pull/2699) [`c0b61c6`](https://github.com/MoonshotAI/kimi-code/commit/c0b61c6e558521fd003de786cad150a3aeb01667) Thanks [@sailist](https://github.com/sailist)! - Fix the token counts reported after compaction reading far below the real context size; they now match the numbers shown while the session runs. + +- [#2810](https://github.com/MoonshotAI/kimi-code/pull/2810) [`64abebc`](https://github.com/MoonshotAI/kimi-code/commit/64abebc95a13b066fefc4f96b062824ea5ec996b) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix multi-select "Other" options so they can be deselected after being committed. + +- [#2701](https://github.com/MoonshotAI/kimi-code/pull/2701) [`7cd6476`](https://github.com/MoonshotAI/kimi-code/commit/7cd64766c8eeff30f3de4bd6467870555d9440db) Thanks [@sailist](https://github.com/sailist)! - Fix multi-second freezes at startup or while idle when a large search index loads, replays, or rebuilds. + +- [#2814](https://github.com/MoonshotAI/kimi-code/pull/2814) [`158c81d`](https://github.com/MoonshotAI/kimi-code/commit/158c81d7055587d582ca424f9b913426fca42559) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Show a clear error message on Windows when Git for Windows is not installed, instead of exiting silently. + +- [#2838](https://github.com/MoonshotAI/kimi-code/pull/2838) [`e5be391`](https://github.com/MoonshotAI/kimi-code/commit/e5be39164b1b47d0b721aad49c41fdf4ec61a7c5) Thanks [@sailist](https://github.com/sailist)! - Close a Windows binary-planting gap in the footer git status: the git and gh commands used for the branch/dirty badge are now resolved to an absolute PATH location, so an executable planted in an untrusted workspace can no longer run before the workspace trust prompt. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add hover tooltips to icon-only buttons. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add breathing room around the fullscreen image preview so images no longer touch the screen edges. + +- [#2740](https://github.com/MoonshotAI/kimi-code/pull/2740) [`01c74e9`](https://github.com/MoonshotAI/kimi-code/commit/01c74e9372fcbbbe99614e859b53b505ed1664a8) Thanks [@oocz](https://github.com/oocz)! - Fix subagent tool changes in one session leaking into builtin profiles in later sessions. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Unify the fullscreen image and video previews with a shared circular close button and the same background overlay. + +- [#2826](https://github.com/MoonshotAI/kimi-code/pull/2826) [`3c9e3b2`](https://github.com/MoonshotAI/kimi-code/commit/3c9e3b297cf5286c761159c1b4d642c478fd394d) Thanks [@liruifengv](https://github.com/liruifengv)! - Page the /sessions picker list so it opens fast with large session counts. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Widen the sidebar's minimum draggable width. + +- [#2723](https://github.com/MoonshotAI/kimi-code/pull/2723) [`e702817`](https://github.com/MoonshotAI/kimi-code/commit/e7028171244789aff58f93da80d477ce3afc939a) Thanks [@sailist](https://github.com/sailist)! - Fix a spurious "Failed to steer" error when sending a message while a goal run is between turns. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Reduce memory and CPU usage when the app stays open for a long time. + +- [#2825](https://github.com/MoonshotAI/kimi-code/pull/2825) [`df8ce73`](https://github.com/MoonshotAI/kimi-code/commit/df8ce73e45e3c473cb58e69311c1213e327f0c01) Thanks [@liruifengv](https://github.com/liruifengv)! - Show retry progress in the loading indicator when a model request fails and is retried, with the attempt count and a detail line for the provider error. + +- [#2840](https://github.com/MoonshotAI/kimi-code/pull/2840) [`68ce3c7`](https://github.com/MoonshotAI/kimi-code/commit/68ce3c7a0ccffe32b51d4fdb57cdeced3931ddcc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Reduce memory usage and stutter during long sessions. + +- [#2837](https://github.com/MoonshotAI/kimi-code/pull/2837) [`101c4d1`](https://github.com/MoonshotAI/kimi-code/commit/101c4d199746bf2ed4f26375b65a6fcb6cba2a60) Thanks [@sailist](https://github.com/sailist)! - Remove the Agent and AgentSwarm tools from the built-in coder subagent profile, so coder subagents no longer delegate further by default. Custom profiles that list these tools explicitly can still opt in. + +- [#2695](https://github.com/MoonshotAI/kimi-code/pull/2695) [`71ff2a0`](https://github.com/MoonshotAI/kimi-code/commit/71ff2a0fffb2ebf399194436ef2d4b599c9988ad) Thanks [@sailist](https://github.com/sailist)! - Fix a Windows security risk where commands launched before the workspace trust prompt could run a malicious executable placed in the current folder. + +- [#2813](https://github.com/MoonshotAI/kimi-code/pull/2813) [`619564d`](https://github.com/MoonshotAI/kimi-code/commit/619564dcf9ee10a3cfbf7ecbc764c6b9b63fc91b) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix the web UI repeatedly losing its realtime connection every ~30 seconds when the server runs behind a reverse proxy or gateway with an idle connection timeout; the server now sends a WebSocket heartbeat and only closes connections that stop responding entirely. + +- [#2842](https://github.com/MoonshotAI/kimi-code/pull/2842) [`e476c5a`](https://github.com/MoonshotAI/kimi-code/commit/e476c5a8bbe68fb0b6eb0096aa1efcb893b1a8fc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add the Modern Web Guidance plugin to the bundled plugin marketplace. Run /plugins and select Modern Web Guidance to install it. + +- Thanks [@Leakless](https://github.com/Leakless) and [@winmin](https://github.com/winmin) for reporting the Windows binary-planting issues fixed in this release. + +## 0.34.0 + +### Minor Changes + +- [#2646](https://github.com/MoonshotAI/kimi-code/pull/2646) [`3c75a27`](https://github.com/MoonshotAI/kimi-code/commit/3c75a27da66e522ae670ec8ce9093ea71d091d27) Thanks [@liruifengv](https://github.com/liruifengv)! - Show a cache-expiry reminder when resuming a long-idle session or submitting after a long idle stretch. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: When a model request fails and interrupts a conversation, a persistent failure card now stays in the session with one-click resume. + +- [#2652](https://github.com/MoonshotAI/kimi-code/pull/2652) [`68ba740`](https://github.com/MoonshotAI/kimi-code/commit/68ba740ebfb3e32ad9abdb8607f48d4387cf6f69) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add Windows support for the built-in Kimi Computer Use capability and show the underlying error when capability setup fails. Install it from `/plugins` on Windows x64. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add a flat view to the sidebar session list. + +### Patch Changes + +- [#2648](https://github.com/MoonshotAI/kimi-code/pull/2648) [`d1ded01`](https://github.com/MoonshotAI/kimi-code/commit/d1ded01b7c50c9847440f4645fe13f588becdc66) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Restore how the last turn ended (completed, cancelled, or failed) when a session is resumed after a server restart, so clients can still surface a previously failed turn instead of the session looking silently stopped. + +- [#2666](https://github.com/MoonshotAI/kimi-code/pull/2666) [`335588e`](https://github.com/MoonshotAI/kimi-code/commit/335588e2594a61a767ce258b34b4049a32b18fe5) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Session listings keep how the last turn ended (completed, cancelled, or failed) across server restarts, so clients can mark previously failed sessions before they are opened. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the model picker overflowing the screen when many models are available, leaving the bottom options unreachable. + +- [#2639](https://github.com/MoonshotAI/kimi-code/pull/2639) [`8588121`](https://github.com/MoonshotAI/kimi-code/commit/858812193a267fcb9382e351137f892646ef79aa) Thanks [@7Sageer](https://github.com/7Sageer)! - /feedback now works for any signed-in user regardless of the active model; signed-out users are shown the sign-up page and GitHub Issues links instead. + +- [#2675](https://github.com/MoonshotAI/kimi-code/pull/2675) [`34c4181`](https://github.com/MoonshotAI/kimi-code/commit/34c418143759a9e80cdda97e95e609c5a993916d) Thanks [@sailist](https://github.com/sailist)! - Fix kimi -p exiting right after the main turn instead of waiting for background tasks and subagents to finish. + +- [#2694](https://github.com/MoonshotAI/kimi-code/pull/2694) [`02c026d`](https://github.com/MoonshotAI/kimi-code/commit/02c026d4871a14cd5e7b4b0e0ec71ba815f643df) Thanks [@sailist](https://github.com/sailist)! - Keep live sessions stable when an MCP server is removed from the workspace config or uninstalled with its plugin: its tools stay registered in open sessions but calls fail with a removal notice, and the MCP panel shows the removed status. Servers added mid-session — by a plugin install or a config edit — are not registered in open sessions; they take effect in new sessions or after `/new` or `/reload`. + +- [#2647](https://github.com/MoonshotAI/kimi-code/pull/2647) [`7bd3fd9`](https://github.com/MoonshotAI/kimi-code/commit/7bd3fd9f6e6c10f88d33b85760631ad6212b5f58) Thanks [@sailist](https://github.com/sailist)! - Read UTF-16 LE/BE text files (with or without a BOM) by transcoding them to UTF-8 instead of refusing them as binary; the web UI file viewer displays them as text as well. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: The sidebar error marker now only appears when the last turn failed; manually cancelled sessions are no longer flagged. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix attachments being silently dropped when sent together with a skill command. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix a manually chosen thinking level being reset to the model default when the first message of a new session is a skill command. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix session renaming during IME composition — Enter no longer submits mid-composition and Esc no longer exits the editor while composing. + +- [#2686](https://github.com/MoonshotAI/kimi-code/pull/2686) [`ef61084`](https://github.com/MoonshotAI/kimi-code/commit/ef610840098a57819d62d407f33256e14b512c77) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Use a compatible PowerShell for Windows Kimi Computer Use installation, provide actionable recovery for locked plugin files, and keep its marketplace name consistent after installation. + +- [#2679](https://github.com/MoonshotAI/kimi-code/pull/2679) [`7b2784b`](https://github.com/MoonshotAI/kimi-code/commit/7b2784b9b7bf4749058da48923ecbbc8019eb7af) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Subagent tasks now show the model and thinking level they use. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix text selection while renaming a session or workspace — dragging no longer moves the whole list item. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the chevron direction on the "show less" button of the changed-files summary card. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: During automatic retries after a failed model request, the working status now shows retry progress (attempt N of M) instead of looking unresponsive. + +- [#2677](https://github.com/MoonshotAI/kimi-code/pull/2677) [`713bf1a`](https://github.com/MoonshotAI/kimi-code/commit/713bf1a5a2b388e4c5f9d3f471a728b8edbf5811) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix resumed sessions rendering background task completion notifications as raw protocol text instead of a task status card. + +- [#2692](https://github.com/MoonshotAI/kimi-code/pull/2692) [`03aa66c`](https://github.com/MoonshotAI/kimi-code/commit/03aa66ca0cca5880dc3a4a89e4f46d09acbe47ae) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Show browser extension links and activation steps after installing Kimi WebBridge. + +- [#2645](https://github.com/MoonshotAI/kimi-code/pull/2645) [`2b89373`](https://github.com/MoonshotAI/kimi-code/commit/2b893733f9853dc0aaeb775d9670d277db8e0381) Thanks [@sailist](https://github.com/sailist)! - Fix the web UI opening the Documents folder instead of the requested file on Windows when the file path contains spaces. + +- [#2697](https://github.com/MoonshotAI/kimi-code/pull/2697) [`e6e4ba2`](https://github.com/MoonshotAI/kimi-code/commit/e6e4ba2357cc659ebd0fd44c9492b498adc33d0e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the background-tasks and todos pills being pushed to the top of the window when the plan approval dialog expands. + +## 0.33.0 + +### Minor Changes + +- [#2407](https://github.com/MoonshotAI/kimi-code/pull/2407) [`0abcd00`](https://github.com/MoonshotAI/kimi-code/commit/0abcd00f7fd3e3cbf087509ffef1c54a6f8d396d) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add Kimi Computer Use and Kimi WebBridge as built-in official marketplace entries in the v2 CLI. Installing from `/plugins` sets up the latest managed runtime and plugin together, reports incomplete manual steps, and supports retrying interrupted setup. + +- [#2627](https://github.com/MoonshotAI/kimi-code/pull/2627) [`f881cdd`](https://github.com/MoonshotAI/kimi-code/commit/f881cdd97073475c43272ec5734bbc39290dd399) Thanks [@sailist](https://github.com/sailist)! - Run the CLI surfaces (interactive TUI, `kimi -p`, `kimi acp`, `kimi export`, `kimi provider`) on the agent-core-v2 engine by default. Set `KIMI_CODE_LEGACY_FLAG=1` to fall back to the legacy engine. + +- [#2565](https://github.com/MoonshotAI/kimi-code/pull/2565) [`54c04bf`](https://github.com/MoonshotAI/kimi-code/commit/54c04bf03ddbeb46d02b2edb460ea091ae194509) Thanks [@7Sageer](https://github.com/7Sageer)! - `/fork` no longer switches to the forked session: the current session stays active and its background tasks keep running. Find the fork in `/sessions`. + +- [#2630](https://github.com/MoonshotAI/kimi-code/pull/2630) [`3bd098b`](https://github.com/MoonshotAI/kimi-code/commit/3bd098b80643c99eabdc602b767dbc53fc47cedd) Thanks [@liruifengv](https://github.com/liruifengv)! - Ask whether to trust the current folder on startup. + +- [#2630](https://github.com/MoonshotAI/kimi-code/pull/2630) [`3bd098b`](https://github.com/MoonshotAI/kimi-code/commit/3bd098b80643c99eabdc602b767dbc53fc47cedd) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add and manage custom providers in settings. + +- [#2599](https://github.com/MoonshotAI/kimi-code/pull/2599) [`541ddd2`](https://github.com/MoonshotAI/kimi-code/commit/541ddd2d898c4880a312874b1c539f85888bf0c1) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Overhaul the UI/UX and fix known issues. + +### Patch Changes + +- [#2601](https://github.com/MoonshotAI/kimi-code/pull/2601) [`75fe068`](https://github.com/MoonshotAI/kimi-code/commit/75fe068a01261ff6b34f176530b338ec6a24918e) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix built-in capability availability and installed status in `/plugins`, preserve legacy WebBridge skills as backups during updates, and prevent Computer Use updates from duplicating or disconnecting MCP servers. + +- [#2635](https://github.com/MoonshotAI/kimi-code/pull/2635) [`2b3e9a9`](https://github.com/MoonshotAI/kimi-code/commit/2b3e9a9f7910b0bb8050380068fa122c2c2cee91) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Rename the partner plugin marketplace tab to Curated and clarify that it contains third-party plugins from Kimi partners. + +- [#2614](https://github.com/MoonshotAI/kimi-code/pull/2614) [`8db7d42`](https://github.com/MoonshotAI/kimi-code/commit/8db7d42f23472a692eb389a0e0e5a3e18aa1b94d) Thanks [@RealKai42](https://github.com/RealKai42)! - Add /bug as an alias for the /feedback slash command. Type /bug to submit feedback. + +- [#2586](https://github.com/MoonshotAI/kimi-code/pull/2586) [`278b6af`](https://github.com/MoonshotAI/kimi-code/commit/278b6af19d8708ec0f3eb2696d62a8d8209d497d) Thanks [@7Sageer](https://github.com/7Sageer)! - Ensure the first request waits for MCP startup to finish while the interface still opens immediately. + +- [#2620](https://github.com/MoonshotAI/kimi-code/pull/2620) [`2ee6e43`](https://github.com/MoonshotAI/kimi-code/commit/2ee6e431240a4a31034e0a403011dd6b2bfef9df) Thanks [@xpzouying](https://github.com/xpzouying)! - Fixed MCP OAuth re-authorization always failing with "Invalid redirect URI": the OAuth callback listener binds a random port per flow, but the dynamic client registration recorded the first flow's port, so every later interactive authorization was rejected at the authorization endpoint. A stale registration is now dropped automatically and the flow re-registers with the current callback URI. + +- [#2596](https://github.com/MoonshotAI/kimi-code/pull/2596) [`c32e661`](https://github.com/MoonshotAI/kimi-code/commit/c32e661faa931df9fdc72e63230f3ebebc00dce5) Thanks [@xpzouying](https://github.com/xpzouying)! - MCP tool results now surface the spec-defined `structuredContent` field and `_meta` server metadata to the model as a serialized `<mcp-structured-result>` block, instead of silently dropping them. Servers that return their machine-readable contract in these fields work the same as on other MCP hosts. + +- [#2612](https://github.com/MoonshotAI/kimi-code/pull/2612) [`e357028`](https://github.com/MoonshotAI/kimi-code/commit/e3570280bde775a153ee04388393506da7ac4cc1) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix all tool calls failing with spawn EBADF on macOS when a skill folder contains a very large file tree. + +- [#2630](https://github.com/MoonshotAI/kimi-code/pull/2630) [`3bd098b`](https://github.com/MoonshotAI/kimi-code/commit/3bd098b80643c99eabdc602b767dbc53fc47cedd) Thanks [@liruifengv](https://github.com/liruifengv)! - Start the interactive TUI without creating a session. + +- [#2630](https://github.com/MoonshotAI/kimi-code/pull/2630) [`3bd098b`](https://github.com/MoonshotAI/kimi-code/commit/3bd098b80643c99eabdc602b767dbc53fc47cedd) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Show the signed-in account and plan usage. + +- [#2630](https://github.com/MoonshotAI/kimi-code/pull/2630) [`3bd098b`](https://github.com/MoonshotAI/kimi-code/commit/3bd098b80643c99eabdc602b767dbc53fc47cedd) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Set an emoji for the session title. + +- [#2630](https://github.com/MoonshotAI/kimi-code/pull/2630) [`3bd098b`](https://github.com/MoonshotAI/kimi-code/commit/3bd098b80643c99eabdc602b767dbc53fc47cedd) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Pin sessions to the top of the sidebar. + +## 0.32.0 + +### Minor Changes + +- [#2558](https://github.com/MoonshotAI/kimi-code/pull/2558) [`75395f6`](https://github.com/MoonshotAI/kimi-code/commit/75395f6abb17f83f30d16b51f4e060a639f43622) Thanks [@sailist](https://github.com/sailist)! - Add the TurnStarted, UserPromptQueued, TaskStarted, and SessionHeartbeat hook events, enrich hook payloads with the session title and client type, include the model and profile in SessionStart, and report SessionEnd as archive when a session is archived instead of exited. Configure the new events under [[hooks]] in config.toml. + +### Patch Changes + +- [#2416](https://github.com/MoonshotAI/kimi-code/pull/2416) [`eaab2b6`](https://github.com/MoonshotAI/kimi-code/commit/eaab2b6f28c0b958edf8ab5ae5e78a4c0426af26) Thanks [@mangeshraut712](https://github.com/mangeshraut712)! - Fall back to the built-in models.dev catalog snapshot when the public catalog is unreachable, so Known third-party provider import still works offline or in blocked networks. + +- [#2083](https://github.com/MoonshotAI/kimi-code/pull/2083) [`bfa0080`](https://github.com/MoonshotAI/kimi-code/commit/bfa00807c975fdc5b84dda32d47b16b09e8d42c1) Thanks [@StaR4y](https://github.com/StaR4y)! - web: Fix dark-mode monochrome controls and align the chat composer corner radius with the design system. + +- [#2559](https://github.com/MoonshotAI/kimi-code/pull/2559) [`dfc55a5`](https://github.com/MoonshotAI/kimi-code/commit/dfc55a5c977dbff657e1da74ff5c2b9d488807be) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Render the "/login" already-logged-in confirmation in the success color instead of dim text, so the "Already logged in. Model configuration refreshed." message is clearly visible. + +- [#2572](https://github.com/MoonshotAI/kimi-code/pull/2572) [`6ba75a1`](https://github.com/MoonshotAI/kimi-code/commit/6ba75a173b595904bc70d0d7161de2f9b964c961) Thanks [@sailist](https://github.com/sailist)! - Rename the `[loop_control] max_retries_per_step` config key to `max_attempts_per_step` and `max_steps_per_run` to `max_steps_per_turn`: on the v2 engine the old keys no longer take effect and a startup warning prompts the rename in `config.toml`. The `KIMI_LOOP_MAX_RETRIES_PER_STEP` env var is likewise deprecated in favor of `KIMI_LOOP_MAX_ATTEMPTS_PER_STEP` but keeps working with a warning. + +- [#2585](https://github.com/MoonshotAI/kimi-code/pull/2585) [`c396873`](https://github.com/MoonshotAI/kimi-code/commit/c39687318c64bf8a305a10bf9ca86ef6ef2c6656) Thanks [@sailist](https://github.com/sailist)! - Fix submitting answers to interactive question prompts being rejected when the model provider returns tool call IDs containing colons (some OpenAI-compatible gateways). + +- [#2562](https://github.com/MoonshotAI/kimi-code/pull/2562) [`071b6a5`](https://github.com/MoonshotAI/kimi-code/commit/071b6a50d9c2ce9c4b45dc4d58dac1101b8c4f52) Thanks [@sailist](https://github.com/sailist)! - Serve v1 message history from the server layer and drop the engine-side legacy message adapter; the /api/v1 message contract is unchanged. + +- [#2562](https://github.com/MoonshotAI/kimi-code/pull/2562) [`071b6a5`](https://github.com/MoonshotAI/kimi-code/commit/071b6a50d9c2ce9c4b45dc4d58dac1101b8c4f52) Thanks [@sailist](https://github.com/sailist)! - Assemble the session snapshot endpoint from the engine's services for both cold and live sessions, and remove the KIMI_SNAPSHOT_READER, KIMI_SNAPSHOT_TIMEOUT_MS, and KIMI_SNAPSHOT_CACHE_LIMIT environment knobs. + +- [#2563](https://github.com/MoonshotAI/kimi-code/pull/2563) [`2118544`](https://github.com/MoonshotAI/kimi-code/commit/21185447fe0f04dbe342bebb6c6d0b364fd43daa) Thanks [@sailist](https://github.com/sailist)! - Fix the context window limit showing as 0 in session status updates when no model is bound yet or the configured model no longer resolves; the limit now falls back to the default model or is omitted when unknown. + +- [#2563](https://github.com/MoonshotAI/kimi-code/pull/2563) [`2118544`](https://github.com/MoonshotAI/kimi-code/commit/21185447fe0f04dbe342bebb6c6d0b364fd43daa) Thanks [@sailist](https://github.com/sailist)! - The `[token_counting]` strategy now only selects the reported context size: `estimated` keeps provider-reported usage out of the context-size display, and `measured` no longer gets stuck retrying an oversized compaction request until it fails. + +- [#2563](https://github.com/MoonshotAI/kimi-code/pull/2563) [`2118544`](https://github.com/MoonshotAI/kimi-code/commit/21185447fe0f04dbe342bebb6c6d0b364fd43daa) Thanks [@sailist](https://github.com/sailist)! - Add a `[token_counting]` config section to choose how context token counts are derived: `measured+estimated` (default), `measured` (provider usage only), or `estimated` (heuristic only, for providers without usage reporting). Set `strategy` under `[token_counting]` in config.toml (or `KIMI_TOKEN_COUNTING_STRATEGY`) to switch. + +## 0.31.1 + +### Patch Changes + +- [#2410](https://github.com/MoonshotAI/kimi-code/pull/2410) [`f1a3475`](https://github.com/MoonshotAI/kimi-code/commit/f1a3475ad5d6540447496701aa75fd4b035ecb28) Thanks [@sailist](https://github.com/sailist)! - Fix sporadic "model is not configured" errors when starting kimi web, caused by the background provider-model refresh transiently clearing the model catalog while the first session was being created. + +- [#2400](https://github.com/MoonshotAI/kimi-code/pull/2400) [`1f3f5da`](https://github.com/MoonshotAI/kimi-code/commit/1f3f5dadaaa4a1d705cc98aee1dbbef13680502c) Thanks [@7Sageer](https://github.com/7Sageer)! - Preserve the assistant's partial output when a turn is interrupted with Esc, and remind the model that the previous turn was deliberately interrupted. + +- [#2415](https://github.com/MoonshotAI/kimi-code/pull/2415) [`5c0ec29`](https://github.com/MoonshotAI/kimi-code/commit/5c0ec2938ac3a01624b6503e5e5df80c9b08f46a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Enable Monaco-based highlighting for code blocks, and fix line numbers overlapping or drifting out of alignment in fallback-rendered code blocks. + +- [#2442](https://github.com/MoonshotAI/kimi-code/pull/2442) [`bb2919e`](https://github.com/MoonshotAI/kimi-code/commit/bb2919eb818a6cb51c71cbabf1bac9020131bce7) Thanks [@liruifengv](https://github.com/liruifengv)! - Reduce frequent full-screen redraws in the TUI. + +- [#2125](https://github.com/MoonshotAI/kimi-code/pull/2125) [`e111c87`](https://github.com/MoonshotAI/kimi-code/commit/e111c878fd5cd07994e125b9e4e07e4069f01be1) Thanks [@bowenliang123](https://github.com/bowenliang123)! - web: Order permission modes from safest to most permissive across settings surfaces, and fix the swapped yolo/auto risk colors in the status panel and mobile settings. + +- [#2459](https://github.com/MoonshotAI/kimi-code/pull/2459) [`326e1fb`](https://github.com/MoonshotAI/kimi-code/commit/326e1fb6ce59fbf2d6c7646e6d587759565814fd) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix chat code blocks rendering in the proportional UI font at the wrong size after the markdown renderer upgrade, and align the loading fallback with the highlighted block so the upgrade no longer shifts layout. + +- [#2437](https://github.com/MoonshotAI/kimi-code/pull/2437) [`ed7a4cc`](https://github.com/MoonshotAI/kimi-code/commit/ed7a4cc095e1619e4dbb6c2c77c89a52e312b085) Thanks [@sailist](https://github.com/sailist)! - web: Make the @ file mention work in a new-session draft, before the first prompt creates the session. + +- [#2437](https://github.com/MoonshotAI/kimi-code/pull/2437) [`ed7a4cc`](https://github.com/MoonshotAI/kimi-code/commit/ed7a4cc095e1619e4dbb6c2c77c89a52e312b085) Thanks [@sailist](https://github.com/sailist)! - web: Fix new sessions showing the thinking level (e.g. Max) while the first message actually ran with thinking off. + +## 0.31.0 + +### Minor Changes + +- [#2365](https://github.com/MoonshotAI/kimi-code/pull/2365) [`fa2c5ce`](https://github.com/MoonshotAI/kimi-code/commit/fa2c5ce18b70577fa3ada4eb8bdd4993891994ce) Thanks [@7Sageer](https://github.com/7Sageer)! - Add support for plugin-contributed custom agents, discovered automatically and available for sub-agent delegation. Ship an `agents/` directory in the plugin (or declare `agents` paths in the plugin manifest) to provide them. + +- [#2314](https://github.com/MoonshotAI/kimi-code/pull/2314) [`02d77b2`](https://github.com/MoonshotAI/kimi-code/commit/02d77b20d941873563f14890e049ffe40cec76e4) Thanks [@7Sageer](https://github.com/7Sageer)! - Allow enabled plugins to contribute agent system-prompt instructions through `systemPrompt` or `systemPromptPath` in `kimi.plugin.json`, effective on both agent engines (the TUI, `kimi -p`, and `kimi web`). + +- [#2232](https://github.com/MoonshotAI/kimi-code/pull/2232) [`efac96c`](https://github.com/MoonshotAI/kimi-code/commit/efac96c8a95a3c3ca4e1ae9bce38082498a02b2e) Thanks [@7Sageer](https://github.com/7Sageer)! - Support Markdown-defined custom agents on agent-core. + +- [#2232](https://github.com/MoonshotAI/kimi-code/pull/2232) [`efac96c`](https://github.com/MoonshotAI/kimi-code/commit/efac96c8a95a3c3ca4e1ae9bce38082498a02b2e) Thanks [@7Sageer](https://github.com/7Sageer)! - Add the /secondary_model slash command to configure the secondary model used by subagents. + +### Patch Changes + +- [#2382](https://github.com/MoonshotAI/kimi-code/pull/2382) [`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix request headers not being passed correctly on some requests. + +- [#2379](https://github.com/MoonshotAI/kimi-code/pull/2379) [`691ec46`](https://github.com/MoonshotAI/kimi-code/commit/691ec4679ea19d6be8ac18f359088384ed3e446d) Thanks [@RealKai42](https://github.com/RealKai42)! - Remove the blocking `block`/`timeout` wait from the TaskOutput tool so checking a background task can no longer stall the conversation; it now always returns an immediate snapshot, and completion still arrives via automatic notification. + +- [#2395](https://github.com/MoonshotAI/kimi-code/pull/2395) [`d10b1c1`](https://github.com/MoonshotAI/kimi-code/commit/d10b1c130813dbd6ee8c8599a6a98feb36aea67f) Thanks [@sailist](https://github.com/sailist)! - Fix sessions missing from the session picker when their cached metadata predates the archived flag. + +## 0.30.0 + +### Minor Changes + +- [#2255](https://github.com/MoonshotAI/kimi-code/pull/2255) [`67dd031`](https://github.com/MoonshotAI/kimi-code/commit/67dd03149f36be91a0c081e70d8a2d721b0f1c64) Thanks [@he-yufeng](https://github.com/he-yufeng)! - Add a customizable footer status line, configured via `[status_line]` in `tui.toml`. + +### Patch Changes + +- [#2313](https://github.com/MoonshotAI/kimi-code/pull/2313) [`de0ba9d`](https://github.com/MoonshotAI/kimi-code/commit/de0ba9d0654273ff6b028a7a561983ebee4e723e) Thanks [@starquakee](https://github.com/starquakee)! - Stop the turn after repeated invalid tool calls instead of retrying indefinitely. + +- [#2147](https://github.com/MoonshotAI/kimi-code/pull/2147) [`29783e4`](https://github.com/MoonshotAI/kimi-code/commit/29783e471afcf7975852e496907646458264d2e6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Show a quota note after installing official plugins that bill against plan quota (such as Kimi Datasource). + +- [#2147](https://github.com/MoonshotAI/kimi-code/pull/2147) [`29783e4`](https://github.com/MoonshotAI/kimi-code/commit/29783e471afcf7975852e496907646458264d2e6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Show a notice when an official plugin used in the session has an update available. Run /plugins to install it. + +- [#1857](https://github.com/MoonshotAI/kimi-code/pull/1857) [`cdbd33c`](https://github.com/MoonshotAI/kimi-code/commit/cdbd33c13c7f5cd4c49ec112ee4313b3938a7752) Thanks [@vinlee19](https://github.com/vinlee19)! - Fail fast when account quota or balance is exhausted instead of silently retrying for ~3 minutes. + +- [#2294](https://github.com/MoonshotAI/kimi-code/pull/2294) [`425cfdf`](https://github.com/MoonshotAI/kimi-code/commit/425cfdf53f0fd3b01527f5fba87acff68f49f368) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix garbled line numbers in code blocks. + +- [#2312](https://github.com/MoonshotAI/kimi-code/pull/2312) [`d03a488`](https://github.com/MoonshotAI/kimi-code/commit/d03a4886fdf7c35014c10079a3d417aeb0447d9a) Thanks [@sailist](https://github.com/sailist)! - Remove the 50 MB size limit on file uploads to the built-in server. + +## 0.29.2 + +### Patch Changes + +- [#2192](https://github.com/MoonshotAI/kimi-code/pull/2192) [`7799bd7`](https://github.com/MoonshotAI/kimi-code/commit/7799bd7346aaee11ec2b6d6883e1e4fe5ab10717) Thanks [@sailist](https://github.com/sailist)! - Hold per-agent runtime state of the experimental engine in the agent-scope state container, so it is observable in one place and disposed with the agent; state snapshots collapse class instances to name markers so resource graphs cannot exhaust memory during export. + +- [#2119](https://github.com/MoonshotAI/kimi-code/pull/2119) [`f06eb5c`](https://github.com/MoonshotAI/kimi-code/commit/f06eb5c60e0a4e51162d1854dda1db41892b457c) Thanks [@pvzheroes125](https://github.com/pvzheroes125)! - Allow hosts to defer registered user-tool schemas until needed. Set `disclosure: "deferred"` when registering a tool. + +- [#2192](https://github.com/MoonshotAI/kimi-code/pull/2192) [`7799bd7`](https://github.com/MoonshotAI/kimi-code/commit/7799bd7346aaee11ec2b6d6883e1e4fe5ab10717) Thanks [@sailist](https://github.com/sailist)! - Instantiate every registered service eagerly at scope creation on the experimental engine, following the dependency graph automatically, and drop the hand-maintained lists that resolved side-effect services one by one at startup. + +- [#2120](https://github.com/MoonshotAI/kimi-code/pull/2120) [`0d00a07`](https://github.com/MoonshotAI/kimi-code/commit/0d00a07c02e334ca904077b2ea8c56cf58b44586) Thanks [@yicun](https://github.com/yicun)! - web: Fix copying selected chat text over plain HTTP from replacing the clipboard with an event placeholder. + +- [#2210](https://github.com/MoonshotAI/kimi-code/pull/2210) [`0cef160`](https://github.com/MoonshotAI/kimi-code/commit/0cef160c4b900a3d78212cd5da4b80d335ea0b6f) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal pursuit being interrupted when a goal turn reaches the per-turn step limit (`loop_control.max_steps_per_turn`); the limit now splits goal work into more continuation turns instead of pausing the goal. + +- [#2153](https://github.com/MoonshotAI/kimi-code/pull/2153) [`c497af6`](https://github.com/MoonshotAI/kimi-code/commit/c497af60e6cd20aab05e590f98a28fb15dd3491d) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix messages sent while a goal is running being rejected with a "Cannot launch a new turn while another turn is active" error; they are now steered into the active goal turn instead of being dropped. + +- [#2192](https://github.com/MoonshotAI/kimi-code/pull/2192) [`7799bd7`](https://github.com/MoonshotAI/kimi-code/commit/7799bd7346aaee11ec2b6d6883e1e4fe5ab10717) Thanks [@sailist](https://github.com/sailist)! - Hold per-session runtime state of the experimental engine in the session-scope state container, so it is observable in one place and disposed with the session. + +- [#2055](https://github.com/MoonshotAI/kimi-code/pull/2055) [`d40d0d3`](https://github.com/MoonshotAI/kimi-code/commit/d40d0d305d2866cb5ab8696e559e0813b5f92201) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix /undo to restore conversation history, todo lists, plan mode, and task notifications consistently. + +## 0.29.1 + +### Patch Changes + +- [#2065](https://github.com/MoonshotAI/kimi-code/pull/2065) [`527d485`](https://github.com/MoonshotAI/kimi-code/commit/527d485d9296fe20f473a4a578d9e6a499c20cd9) Thanks [@7Sageer](https://github.com/7Sageer)! - Add global default MCP server timeouts in `config.toml` and env vars. + +- [#2104](https://github.com/MoonshotAI/kimi-code/pull/2104) [`66f611a`](https://github.com/MoonshotAI/kimi-code/commit/66f611aae99887ad2076aa3482a0df5e415d3511) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix loss of thinking content with OpenAI-compatible endpoints that return reasoning under a different field name (e.g. newer vLLM); the reasoning field is now detected per endpoint and echoed back on follow-up requests. + +- [#2089](https://github.com/MoonshotAI/kimi-code/pull/2089) [`ca38b7e`](https://github.com/MoonshotAI/kimi-code/commit/ca38b7ed864ad5fa2b2e3c8b96d8a7b10a734445) Thanks [@liruifengv](https://github.com/liruifengv)! - Remove the toolbar tip that suggested trying the "superpowers" plugin. + +- [#2064](https://github.com/MoonshotAI/kimi-code/pull/2064) [`7b62ed5`](https://github.com/MoonshotAI/kimi-code/commit/7b62ed5b2c2709719f360c01a2f513dee34ae179) Thanks [@7Sageer](https://github.com/7Sageer)! - Add experimental secondary-model bindings for newly spawned subagents, including per-agent model preferences and subagent-only model overrides. + +- [#2096](https://github.com/MoonshotAI/kimi-code/pull/2096) [`5fdbdb4`](https://github.com/MoonshotAI/kimi-code/commit/5fdbdb4a22b86ae6f7ba7c775741689aaaf215f0) Thanks [@7Sageer](https://github.com/7Sageer)! - Add environment variables to configure the web search and web fetch services without OAuth login. + +## 0.29.0 + +### Minor Changes + +- [#1992](https://github.com/MoonshotAI/kimi-code/pull/1992) [`a8f1ca3`](https://github.com/MoonshotAI/kimi-code/commit/a8f1ca3f1016a3e84986f297367e833bc731ac39) Thanks [@RealKai42](https://github.com/RealKai42)! - Support selecting a thinking effort level from ACP clients: the thinking picker now lists the current model's declared levels (for example off / low / medium / high) instead of only an on/off toggle. Use the thinking selector in your ACP client (e.g. Zed) to pick a level; the legacy on/off values keep working. + +- [#1735](https://github.com/MoonshotAI/kimi-code/pull/1735) [`ce0e3ce`](https://github.com/MoonshotAI/kimi-code/commit/ce0e3ceb04223bdaad8e8931bad46eff561055b6) Thanks [@7Sageer](https://github.com/7Sageer)! - Let custom agent files restrict which sub-agent types they may delegate to (v2 engine only). + +- [#1735](https://github.com/MoonshotAI/kimi-code/pull/1735) [`ce0e3ce`](https://github.com/MoonshotAI/kimi-code/commit/ce0e3ceb04223bdaad8e8931bad46eff561055b6) Thanks [@7Sageer](https://github.com/7Sageer)! - Support custom agents defined as Markdown files with frontmatter, usable as the main agent or a sub-agent (v2 engine only). + +- [#1735](https://github.com/MoonshotAI/kimi-code/pull/1735) [`ce0e3ce`](https://github.com/MoonshotAI/kimi-code/commit/ce0e3ceb04223bdaad8e8931bad46eff561055b6) Thanks [@7Sageer](https://github.com/7Sageer)! - Add global tool gating to constrain which tools agents may use, with a per-session override (v2 engine only). + +- [#2012](https://github.com/MoonshotAI/kimi-code/pull/2012) [`d67a200`](https://github.com/MoonshotAI/kimi-code/commit/d67a2003abf2d8d802dcf24f806e0a811724b83e) Thanks [@sailist](https://github.com/sailist)! - Add a GET /api/v1/fs:content server endpoint that serves any file on the host by absolute path as raw content with Content-Type, ETag, and Range support. + +- [#1999](https://github.com/MoonshotAI/kimi-code/pull/1999) [`4c763f6`](https://github.com/MoonshotAI/kimi-code/commit/4c763f6763acb67a73d133f7450d092e71d63692) Thanks [@RealKai42](https://github.com/RealKai42)! - Videos attached to a prompt — pasted in the TUI or uploaded in the web UI — now reach the model together with the prompt, with no extra tool round trip, and stay playable in the chat after a reload. + +- [#1735](https://github.com/MoonshotAI/kimi-code/pull/1735) [`ce0e3ce`](https://github.com/MoonshotAI/kimi-code/commit/ce0e3ceb04223bdaad8e8931bad46eff561055b6) Thanks [@7Sageer](https://github.com/7Sageer)! - Support overriding the default main-agent system prompt with a user-level file for every session (v2 engine only). + +### Patch Changes + +- [#1997](https://github.com/MoonshotAI/kimi-code/pull/1997) [`74da87a`](https://github.com/MoonshotAI/kimi-code/commit/74da87a457c2964694a844dd22a4925f5113b167) Thanks [@sailist](https://github.com/sailist)! - Add agent.created and agent.disposed events to the server session event stream, and expose each agent's disposal time in the transcript API. + +- [#2030](https://github.com/MoonshotAI/kimi-code/pull/2030) [`ec88d35`](https://github.com/MoonshotAI/kimi-code/commit/ec88d352e8f4dc5e8ffd1212f016138458f69893) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix catalog-imported Claude models being wrongly locked into always-on thinking, and stop offering a misleading thinking Off option for models that cannot truly disable reasoning (such as Gemini 3). Also normalizes configured thinking effort values and unifies context-usage reporting. + +- [#2015](https://github.com/MoonshotAI/kimi-code/pull/2015) [`b5efba7`](https://github.com/MoonshotAI/kimi-code/commit/b5efba7abcaf4041f81ec520097a61e6546e8c50) Thanks [@RealKai42](https://github.com/RealKai42)! - Import many more providers from the models.dev catalog: vendor SDKs like xai and openrouter now import instead of being refused (with a "guessed" note), deprecated and alpha models are filtered out, per-model gateway protocol and endpoint overrides are honored, and context limits are correct (input limit for compaction, total window for completion). Imports lacking a usable endpoint now ask for one via `--base-url` or a prompt. + +- [#1993](https://github.com/MoonshotAI/kimi-code/pull/1993) [`37eda4e`](https://github.com/MoonshotAI/kimi-code/commit/37eda4e59aebc8ecafa91be3f43f971ed63963a3) Thanks [@RealKai42](https://github.com/RealKai42)! - Add environment variable overrides for agent loop and background task limits. Set KIMI_LOOP_MAX_STEPS_PER_TURN, KIMI_LOOP_MAX_RETRIES_PER_STEP, or KIMI_CODE_BACKGROUND_MAX_RUNNING_TASKS to take priority over the [loop_control] and [background] config. + +- [#1993](https://github.com/MoonshotAI/kimi-code/pull/1993) [`37eda4e`](https://github.com/MoonshotAI/kimi-code/commit/37eda4e59aebc8ecafa91be3f43f971ed63963a3) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix config environment overrides (such as KIMI_IMAGE_MAX_EDGE_PX or KIMI_SUBAGENT_TIMEOUT_MS) being persisted into config.toml by config API writes while the env var is set, and keeping the old value after the env var is changed to an invalid value or removed. + +- [#2050](https://github.com/MoonshotAI/kimi-code/pull/2050) [`8250e59`](https://github.com/MoonshotAI/kimi-code/commit/8250e590f3ed5990c233ef5a2c7666468f0bcb05) Thanks [@sailist](https://github.com/sailist)! - Remove references to the non-existent `kimi resume` command from the scheduled-task tool descriptions. + +- [#1970](https://github.com/MoonshotAI/kimi-code/pull/1970) [`6dd4fd3`](https://github.com/MoonshotAI/kimi-code/commit/6dd4fd33688b37904d5302436fc2daaf09d66c7d) Thanks [@sailist](https://github.com/sailist)! - Fix cancelled model requests being wrapped as retryable provider errors, so interrupting a request no longer triggers silent retries. + +- [#1970](https://github.com/MoonshotAI/kimi-code/pull/1970) [`6dd4fd3`](https://github.com/MoonshotAI/kimi-code/commit/6dd4fd33688b37904d5302436fc2daaf09d66c7d) Thanks [@sailist](https://github.com/sailist)! - Send the session prompt cache key to OpenAI and OpenAI Responses providers, restoring provider-side prompt cache affinity that previously only reached Kimi and Anthropic. + +- [#1999](https://github.com/MoonshotAI/kimi-code/pull/1999) [`4c763f6`](https://github.com/MoonshotAI/kimi-code/commit/4c763f6763acb67a73d133f7450d092e71d63692) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix ReadMediaFile failing on videos when the provider has no file upload channel — such videos now fall back to inline delivery. + +- [#1968](https://github.com/MoonshotAI/kimi-code/pull/1968) [`71bcfba`](https://github.com/MoonshotAI/kimi-code/commit/71bcfba54a6836f4b6d4e26babde67576b293a64) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix sessions getting stuck on every turn with a provider "message must not be empty" error after a content-filtered response. + +- [#2022](https://github.com/MoonshotAI/kimi-code/pull/2022) [`154e082`](https://github.com/MoonshotAI/kimi-code/commit/154e0824880c8573433e4ec7ada083744dbfe9f9) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show transparent images over a checkerboard canvas so white and black content stays visible in both light and dark themes. + +- [#1990](https://github.com/MoonshotAI/kimi-code/pull/1990) [`115b096`](https://github.com/MoonshotAI/kimi-code/commit/115b0968cefede7fac1494c6f0154ea5545a89da) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix goal mode continuation prompts leaking into the transcript when resuming a session. + +- [#1970](https://github.com/MoonshotAI/kimi-code/pull/1970) [`6dd4fd3`](https://github.com/MoonshotAI/kimi-code/commit/6dd4fd33688b37904d5302436fc2daaf09d66c7d) Thanks [@sailist](https://github.com/sailist)! - Rework the model wire layer in the experimental v2 engine into a small set of protocol bases plus declarative provider trait definitions, so adding a provider no longer means copying adapter code, and per-turn request intent (cache key, thinking effort, sampling) flows as request parameters instead of cloned model objects. The never-functional `[platforms]` config section and the `provider.platformId` field are removed; credential resolution is now a two-layer model → provider lookup. + +- [#1976](https://github.com/MoonshotAI/kimi-code/pull/1976) [`e458323`](https://github.com/MoonshotAI/kimi-code/commit/e45832398d0d9cad98dbad1cbf1e5b103a20aace) Thanks [@liruifengv](https://github.com/liruifengv)! - Improve TUI performance and resume speed for long-running sessions. + +- [#1991](https://github.com/MoonshotAI/kimi-code/pull/1991) [`92576e4`](https://github.com/MoonshotAI/kimi-code/commit/92576e4d850ada51a24e72fe76a83cc512df922a) Thanks [@7Sageer](https://github.com/7Sageer)! - Reconnect a dropped MCP server connection automatically when one of its tools is called, and retry the call once. + +- [#1970](https://github.com/MoonshotAI/kimi-code/pull/1970) [`6dd4fd3`](https://github.com/MoonshotAI/kimi-code/commit/6dd4fd33688b37904d5302436fc2daaf09d66c7d) Thanks [@sailist](https://github.com/sailist)! - Add read-only model resolution inspection and a live connectivity probe to the server's RPC surface, reporting per-field value provenance (config, override, builtin, env, synthesized) for internal debugging tools. + +- [#2015](https://github.com/MoonshotAI/kimi-code/pull/2015) [`b5efba7`](https://github.com/MoonshotAI/kimi-code/commit/b5efba7abcaf4041f81ec520097a61e6546e8c50) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix thinking levels being offered for models that do not support them (e.g. phantom levels on Kimi K3): levels now come from each model's declared capabilities. Models that cannot disable reasoning (e.g. gpt-5) no longer offer an Off option, and turning thinking Off on models that support it (e.g. xai grok) now truly disables reasoning. + +- [#1735](https://github.com/MoonshotAI/kimi-code/pull/1735) [`ce0e3ce`](https://github.com/MoonshotAI/kimi-code/commit/ce0e3ceb04223bdaad8e8931bad46eff561055b6) Thanks [@7Sageer](https://github.com/7Sageer)! - Warn when a tool allow/deny list entry can never match any tool, for example a misspelled name (v2 engine only). + +- [#2005](https://github.com/MoonshotAI/kimi-code/pull/2005) [`a3699dd`](https://github.com/MoonshotAI/kimi-code/commit/a3699dd6aa7b41efd3129a117007d195282379fd) Thanks [@7Sageer](https://github.com/7Sageer)! - Add an `active` flag to each tool in the server's tool listing API. + +- [#1995](https://github.com/MoonshotAI/kimi-code/pull/1995) [`73eb5f8`](https://github.com/MoonshotAI/kimi-code/commit/73eb5f89e06fb15d42c7585a147eb1c5caef0725) Thanks [@liruifengv](https://github.com/liruifengv)! - Remove red coloring from syntax highlighting in code previews and markdown code blocks. + +- [#2014](https://github.com/MoonshotAI/kimi-code/pull/2014) [`576d650`](https://github.com/MoonshotAI/kimi-code/commit/576d65038035570bea90b58d5824bcd60ca11258) Thanks [@liruifengv](https://github.com/liruifengv)! - Add a reminder for third-party install sources to use the official installer in the update prompt. + +## 0.28.1 + +### Patch Changes + +- [#934](https://github.com/MoonshotAI/kimi-code/pull/934) [`c5b6103`](https://github.com/MoonshotAI/kimi-code/commit/c5b6103bb9b0a163d48cbce0034c3fc7dea7c344) Thanks [@tt-a1i](https://github.com/tt-a1i)! - Allow ACP sessions to start with configured non-OAuth model credentials instead of requiring terminal login. + +- [#1967](https://github.com/MoonshotAI/kimi-code/pull/1967) [`ad8cc85`](https://github.com/MoonshotAI/kimi-code/commit/ad8cc8525198a08bc1181cee9a15bbb4521cd9bc) Thanks [@sailist](https://github.com/sailist)! - Run web servers foreground-only end to end: the /web slash command now always starts a new server, and the `kimi web kill` / `kimi web ps` subcommands are removed — foreground servers stop with Ctrl+C. `kimi server kill` remains as a deprecated fallback that only stops servers started by a version before 0.28.0. + +- [#1948](https://github.com/MoonshotAI/kimi-code/pull/1948) [`f6f4192`](https://github.com/MoonshotAI/kimi-code/commit/f6f4192957ace3f0cceb734a04b3b26b1d2f88be) Thanks [@sailist](https://github.com/sailist)! - Fix running subagents not observing permission mode switches made after they started. + +## 0.28.0 + +### Minor Changes + +- [#1826](https://github.com/MoonshotAI/kimi-code/pull/1826) [`a41a09c`](https://github.com/MoonshotAI/kimi-code/commit/a41a09c33c8e432fbc306f5882692c967ed5ea17) Thanks [@sailist](https://github.com/sailist)! - Replace the `kimi server` command tree with `kimi web`: the server runs in the foreground (the background daemon and OS-service lifecycle commands are removed), and multiple servers can now share one home directory, each taking the next free port. Manage instances with `kimi web kill [server-id|all]`, `kimi web ps`, and `kimi web rotate-token`; any `kimi server …` invocation prints a deprecation notice and exits 1. + +- [#1933](https://github.com/MoonshotAI/kimi-code/pull/1933) [`11c1683`](https://github.com/MoonshotAI/kimi-code/commit/11c1683a1cd2adab276562419d2d353629063d80) Thanks [@liruifengv](https://github.com/liruifengv)! - Thinking effort persists only levels below the model's top tier (max). + +### Patch Changes + +- [#1867](https://github.com/MoonshotAI/kimi-code/pull/1867) [`3086e47`](https://github.com/MoonshotAI/kimi-code/commit/3086e4703992fbbe7a41379405ee243713ad9ced) Thanks [@RealKai42](https://github.com/RealKai42)! - Rename the stale "afk" reference to "auto" in the built-in MCP config skill guidance. + +- [#1867](https://github.com/MoonshotAI/kimi-code/pull/1867) [`3086e47`](https://github.com/MoonshotAI/kimi-code/commit/3086e4703992fbbe7a41379405ee243713ad9ced) Thanks [@RealKai42](https://github.com/RealKai42)! - Correct the YOLO and Auto permission mode descriptions in CLI --help output and in the ACP session mode selector shown by IDE clients. + +- [#1867](https://github.com/MoonshotAI/kimi-code/pull/1867) [`3086e47`](https://github.com/MoonshotAI/kimi-code/commit/3086e4703992fbbe7a41379405ee243713ad9ced) Thanks [@RealKai42](https://github.com/RealKai42)! - web: Correct the YOLO and Auto permission mode descriptions in the slash command list and the mobile permission sheet. + +- [#1867](https://github.com/MoonshotAI/kimi-code/pull/1867) [`3086e47`](https://github.com/MoonshotAI/kimi-code/commit/3086e4703992fbbe7a41379405ee243713ad9ced) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix the YOLO and Auto permission mode descriptions to match their actual behavior: YOLO auto-approves tool actions but the agent may still ask questions, while Auto is fully autonomous and never asks. + +- [#1867](https://github.com/MoonshotAI/kimi-code/pull/1867) [`3086e47`](https://github.com/MoonshotAI/kimi-code/commit/3086e4703992fbbe7a41379405ee243713ad9ced) Thanks [@RealKai42](https://github.com/RealKai42)! - Correct the YOLO mode notice shown when replaying a session: tool actions are auto-approved, but the agent may still ask questions. + +- [#1843](https://github.com/MoonshotAI/kimi-code/pull/1843) [`a3e773f`](https://github.com/MoonshotAI/kimi-code/commit/a3e773f90ce66abe6db229607440c20769537c93) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix the web backend ignoring symbolic links when loading AGENTS.md files and reading files. + +- [#1940](https://github.com/MoonshotAI/kimi-code/pull/1940) [`d71bf9e`](https://github.com/MoonshotAI/kimi-code/commit/d71bf9e5a56b5978316e715f7c131c784967d562) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add a note in the model switcher that switching models or thinking effort invalidates the existing prompt cache. + +## 0.27.0 + +### Minor Changes + +- [#1822](https://github.com/MoonshotAI/kimi-code/pull/1822) [`a5c568d`](https://github.com/MoonshotAI/kimi-code/commit/a5c568dc7a84962bae70a16858709c453fc90a07) Thanks [@liruifengv](https://github.com/liruifengv)! - Add the /copy slash command to copy the last assistant message to the clipboard. + +- [#1824](https://github.com/MoonshotAI/kimi-code/pull/1824) [`bfecd01`](https://github.com/MoonshotAI/kimi-code/commit/bfecd0128fe7d88971a84095e24ef8a56ba34e71) Thanks [@liruifengv](https://github.com/liruifengv)! - Using an API key for Kimi coding models now also fetches the latest model list automatically. + +### Patch Changes + +- [#1811](https://github.com/MoonshotAI/kimi-code/pull/1811) [`cec15e2`](https://github.com/MoonshotAI/kimi-code/commit/cec15e2188b24e0f904e5ca660a2e72c06364647) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix Esc and Ctrl+C cancelling compaction instead of closing an open /btw panel. + +- [#1806](https://github.com/MoonshotAI/kimi-code/pull/1806) [`9b49694`](https://github.com/MoonshotAI/kimi-code/commit/9b496946dcb3c7fa9507e6d5c251c1941e44a316) Thanks [@sailist](https://github.com/sailist)! - Mount the dev-only /api/v1/debug RPC surface behind the --debug-endpoints flag, exposing every scoped service for local debugging on loopback binds. Pass --debug-endpoints to kimi server run to enable it. + +- [#1788](https://github.com/MoonshotAI/kimi-code/pull/1788) [`365ba00`](https://github.com/MoonshotAI/kimi-code/commit/365ba0001de206863ff1de8e106c85d7f187c192) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix `/export-debug-zip` and `kimi export` overwriting the previous ZIP archive when run repeatedly on the same session; the default export filename now includes a timestamp. + +- [#1840](https://github.com/MoonshotAI/kimi-code/pull/1840) [`fa7e4ba`](https://github.com/MoonshotAI/kimi-code/commit/fa7e4ba4218703bb1ef3112ab2493496983b0539) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix AGENTS.md files installed as symbolic links being ignored by the web backend. + +- [#1829](https://github.com/MoonshotAI/kimi-code/pull/1829) [`1b907b0`](https://github.com/MoonshotAI/kimi-code/commit/1b907b07cdcc0e9cba5203fe40dacae85a4b768d) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix whitespace-only thinking content rendering as a blank bullet line in the transcript, both while streaming and when replaying session history. + +- [#1809](https://github.com/MoonshotAI/kimi-code/pull/1809) [`56a321d`](https://github.com/MoonshotAI/kimi-code/commit/56a321d4d127c0b4cf7a3e15e2959ebf3eded192) Thanks [@sailist](https://github.com/sailist)! - web: Fix duplicate workspace groups on Windows when the same folder is opened with different path spellings, such as a different drive-letter casing; all of the folder's sessions now list under the single merged group. + +- [#1847](https://github.com/MoonshotAI/kimi-code/pull/1847) [`56ba8e0`](https://github.com/MoonshotAI/kimi-code/commit/56ba8e0196a3053ad1115a7e8f8b8c4c0cd1b320) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix LaTeX formulas rendering as garbled overlapping text when the web UI is accessed over the network; the server's content security policy now allows the inline styles that math and code highlighting rely on, while scripts remain strictly restricted. + +- [#1816](https://github.com/MoonshotAI/kimi-code/pull/1816) [`44f3341`](https://github.com/MoonshotAI/kimi-code/commit/44f334191989183d21920f6867c405581347c748) Thanks [@sailist](https://github.com/sailist)! - Harden the embedded key-value engine's durability: WAL compaction now always terminates under sustained write storms instead of chasing the tail forever, a committed write can no longer slip through a compaction rotation undetected, torn WAL tails no longer misplace later disk-mode value pointers, read-only opens never create or modify database files or compact under a live writer, corrupt index-definition files no longer force a full rebuild, stale compaction temp files are cleaned on open, and the process lock can no longer be taken over by several processes at once. + +- [#1816](https://github.com/MoonshotAI/kimi-code/pull/1816) [`44f3341`](https://github.com/MoonshotAI/kimi-code/commit/44f334191989183d21920f6867c405581347c748) Thanks [@sailist](https://github.com/sailist)! - Speed up the embedded key-value engine under stress: queries with skip/limit now stream candidates instead of decoding every match first, LRU eviction picks victims in O(1) instead of scanning every key, bursts of simultaneously expired TTL keys are drained within seconds, existence checks and size counting no longer read values when they only need metadata, and one oversized token can no longer poison the full-text index. + +- [#1816](https://github.com/MoonshotAI/kimi-code/pull/1816) [`44f3341`](https://github.com/MoonshotAI/kimi-code/commit/44f334191989183d21920f6867c405581347c748) Thanks [@sailist](https://github.com/sailist)! - Cluster readers of the embedded key-value engine now catch up incrementally by replaying only newly appended WAL frames after another process writes, instead of fully reopening the shard on every read; cross-process read latency drops by orders of magnitude at larger shard sizes, and readers still fall back to a full reopen after WAL rotation or truncation. + +- [#1816](https://github.com/MoonshotAI/kimi-code/pull/1816) [`44f3341`](https://github.com/MoonshotAI/kimi-code/commit/44f334191989183d21920f6867c405581347c748) Thanks [@sailist](https://github.com/sailist)! - Keep the embedded key-value engine writable when a WAL compaction rotation fails mid-way instead of wedging it until reopen, stop a rolled-back write from erasing a concurrently committed value for the same key, let the RESP server survive aborted connections, recover after oversized requests, and answer each pipelined command independently, and keep the previous full-text index intact when a postings rebuild fails. + +- [#1808](https://github.com/MoonshotAI/kimi-code/pull/1808) [`b53e00d`](https://github.com/MoonshotAI/kimi-code/commit/b53e00db91872efc602743d07d2283f7938eaea2) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Include the underlying network cause (DNS failure, refused connection, TLS or timeout errors) in OAuth connection error messages instead of a bare "fetch failed". + +- [#1790](https://github.com/MoonshotAI/kimi-code/pull/1790) [`373abb0`](https://github.com/MoonshotAI/kimi-code/commit/373abb02f03ef817e2e1937e1cdc4423ef0cd149) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix repeated request rejections after an interrupted model response by recording tool calls that never ran and closing them with an interrupted result. + +- [#1791](https://github.com/MoonshotAI/kimi-code/pull/1791) [`3144972`](https://github.com/MoonshotAI/kimi-code/commit/31449728b72df94e22bcb2de350a1e7624895e30) Thanks [@sailist](https://github.com/sailist)! - Fix the built-in URL fetch tool's network safeguards: crafted domains and redirect chains can no longer reach loopback or internal network services. + +- [#1787](https://github.com/MoonshotAI/kimi-code/pull/1787) [`319001a`](https://github.com/MoonshotAI/kimi-code/commit/319001ae5cde6df383579214a126564b9ed2b114) Thanks [@sailist](https://github.com/sailist)! - web: Remove per-workspace git repo badges and branch labels; branch, PR, and diff status remain shown for the active session. + +- [#1838](https://github.com/MoonshotAI/kimi-code/pull/1838) [`9e12484`](https://github.com/MoonshotAI/kimi-code/commit/9e1248416faa22d9f0b777b91ad092bbf1e19182) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Remember the thinking level per model, fixing an empty and unresponsive thinking picker when the active model does not support a previously stored level. + +- [#1833](https://github.com/MoonshotAI/kimi-code/pull/1833) [`03021b6`](https://github.com/MoonshotAI/kimi-code/commit/03021b6db7166c750dd34043edaa85c423d3202f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix queued messages silently re-sending previously uploaded files when a session is reopened. + +## 0.26.0 + +### Minor Changes + +- [#1776](https://github.com/MoonshotAI/kimi-code/pull/1776) [`ffaf0b9`](https://github.com/MoonshotAI/kimi-code/commit/ffaf0b98ca76bb90ba9c989256441dceb468d85f) Thanks [@sailist](https://github.com/sailist)! - Expand the coder subagent tool set to include background tasks, todo lists, plan mode, skill invocation, and nested agents, mirroring the main agent's capabilities; a subagent run also waits for its background tasks to settle before reporting completion. Applies automatically to coder subagents launched through the Agent tool. + +### Patch Changes + +- [#1771](https://github.com/MoonshotAI/kimi-code/pull/1771) [`b513975`](https://github.com/MoonshotAI/kimi-code/commit/b5139757e2df1b5b8723d4bab5137266f5eb0f01) Thanks [@liruifengv](https://github.com/liruifengv)! - Optimize the unit formatting of the context usage display. + +- [#1765](https://github.com/MoonshotAI/kimi-code/pull/1765) [`d531398`](https://github.com/MoonshotAI/kimi-code/commit/d531398d0143cd3b0a2f4a099ff537894c9245e9) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix Kimi-provider models routed through the Anthropic protocol incorrectly showing reasoning effort options. Effort choices now come only from the model's declared metadata, and the inferred fallback profile applies solely to non-Kimi Anthropic-compatible providers. + +- [#1774](https://github.com/MoonshotAI/kimi-code/pull/1774) [`3d5d630`](https://github.com/MoonshotAI/kimi-code/commit/3d5d630c12ea71fb7066e8018dfa2cb6d42da3e8) Thanks [@RealKai42](https://github.com/RealKai42)! - Honor an explicit thinking "off" on OpenAI-compatible (chat completions) providers: it used to be indistinguishable from "never configured", so the history-based auto `reasoning_effort` injection kept the model reasoning (and could leak the field to models that reject it). The provider now also reports the actual current thinking effort ("on"/"off") instead of recording "off" for both. + +- [#1766](https://github.com/MoonshotAI/kimi-code/pull/1766) [`7042af3`](https://github.com/MoonshotAI/kimi-code/commit/7042af3571dbfbf5600535a56692434b84afb4ce) Thanks [@kermanx](https://github.com/kermanx)! - web: Fix the sidebar resize handle being covered by the chat composer background. + +- [#1769](https://github.com/MoonshotAI/kimi-code/pull/1769) [`d1ca65e`](https://github.com/MoonshotAI/kimi-code/commit/d1ca65e1de189617e9edbc54010e62d472a1de3d) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Keep legacy migrations idempotent across multiple Kimi homes and report damaged or unmapped sessions instead of silently skipping them. + +- [#1763](https://github.com/MoonshotAI/kimi-code/pull/1763) [`81414b6`](https://github.com/MoonshotAI/kimi-code/commit/81414b6ad5eddb64bbd959a8754ffb6c20b4f6fe) Thanks [@liruifengv](https://github.com/liruifengv)! - Warn in the /model and /effort pickers that switching invalidates the existing prompt cache, and hint to use /new to avoid extra token costs. + +- [#1773](https://github.com/MoonshotAI/kimi-code/pull/1773) [`1169a6d`](https://github.com/MoonshotAI/kimi-code/commit/1169a6d5fdafca4c1455c3ac4889586ef42f4435) Thanks [@RealKai42](https://github.com/RealKai42)! - Replay empty thinking content verbatim instead of substituting a placeholder space on Anthropic-compatible and Kimi preserved-thinking endpoints. + +- [#1781](https://github.com/MoonshotAI/kimi-code/pull/1781) [`09e8554`](https://github.com/MoonshotAI/kimi-code/commit/09e855401be62431b967dcb3b7caf1bcc9705df5) Thanks [@kermanx](https://github.com/kermanx)! - Report when users stop tasks and preserve other stop reasons in model context. + +- [#1784](https://github.com/MoonshotAI/kimi-code/pull/1784) [`d465591`](https://github.com/MoonshotAI/kimi-code/commit/d465591eb3fdb30c0c0348d6edb6f4d3d2f72698) Thanks [@sailist](https://github.com/sailist)! - Fix a resumed session being marked as just updated and jumping to the top of the session list without any new activity. + +- [#1759](https://github.com/MoonshotAI/kimi-code/pull/1759) [`9e3e670`](https://github.com/MoonshotAI/kimi-code/commit/9e3e6700f9276f4ab60219897b297fc96be2355a) Thanks [@sailist](https://github.com/sailist)! - Fix a race where resuming a background subagent right after it was manually stopped could fail with an "already running" error. + +- [#1782](https://github.com/MoonshotAI/kimi-code/pull/1782) [`072eed4`](https://github.com/MoonshotAI/kimi-code/commit/072eed476b5fe7599d994783649a21083320df58) Thanks [@sailist](https://github.com/sailist)! - Fix the context size indicator under-reporting the model's actual context usage. + +- [#1769](https://github.com/MoonshotAI/kimi-code/pull/1769) [`d1ca65e`](https://github.com/MoonshotAI/kimi-code/commit/d1ca65e1de189617e9edbc54010e62d472a1de3d) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Support in-process editor hosts with session lifecycle, context, MCP configuration, and cross-platform session storage APIs. + +- [#1772](https://github.com/MoonshotAI/kimi-code/pull/1772) [`78967e2`](https://github.com/MoonshotAI/kimi-code/commit/78967e283d28238337e6437b4824f67b2c3cea7d) Thanks [@sailist](https://github.com/sailist)! - web: Refresh the model catalog for all providers when opening the model picker, so newly available models always show up. + +## 0.25.0 + +### Minor Changes + +- [#1731](https://github.com/MoonshotAI/kimi-code/pull/1731) [`0b790cd`](https://github.com/MoonshotAI/kimi-code/commit/0b790cdc056475593abd572f657d010504caf752) Thanks [@sailist](https://github.com/sailist)! - web: Allow attaching any file type in chat; files the model cannot consume inline (documents, SVG images, archives, …) are uploaded to the server and given to the model as a file path it can read on demand. + +### Patch Changes + +- [#1746](https://github.com/MoonshotAI/kimi-code/pull/1746) [`918c135`](https://github.com/MoonshotAI/kimi-code/commit/918c1354d9ff4a7dc66a02ede3a504d19e1f53d1) Thanks [@RealKai42](https://github.com/RealKai42)! - Honor adaptive_thinking = false on Anthropic-compatible models by limiting thinking efforts to the legacy budget set and omitting the effort parameter from requests. + +- [#1746](https://github.com/MoonshotAI/kimi-code/pull/1746) [`918c135`](https://github.com/MoonshotAI/kimi-code/commit/918c1354d9ff4a7dc66a02ede3a504d19e1f53d1) Thanks [@RealKai42](https://github.com/RealKai42)! - Apply official Anthropic effort profiles and a 128k output fallback for unknown models. Preserve compatible-provider thinking history across session resumes and model switches, normalize incomplete stream events, and warn on unlisted efforts. + +- [#1746](https://github.com/MoonshotAI/kimi-code/pull/1746) [`918c135`](https://github.com/MoonshotAI/kimi-code/commit/918c1354d9ff4a7dc66a02ede3a504d19e1f53d1) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix custom-named models on Anthropic-compatible providers starting new sessions with thinking effort off instead of the model default, and not showing the thinking control in ACP clients. + +- [#1757](https://github.com/MoonshotAI/kimi-code/pull/1757) [`f0c8a10`](https://github.com/MoonshotAI/kimi-code/commit/f0c8a103c620b4a66761c0f34c1a8cc7ece9b86c) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix the diagnostic log missing the actual error when the CLI exits unexpectedly. + +- [#1731](https://github.com/MoonshotAI/kimi-code/pull/1731) [`0b790cd`](https://github.com/MoonshotAI/kimi-code/commit/0b790cdc056475593abd572f657d010504caf752) Thanks [@sailist](https://github.com/sailist)! - Fix the Content-Security-Policy on non-loopback server binds blocking the web UI's theme bootstrap script and bundled fonts, and tighten the policy with explicit form-action, base-uri, and frame-ancestors directives. + +- [#1758](https://github.com/MoonshotAI/kimi-code/pull/1758) [`1d7c205`](https://github.com/MoonshotAI/kimi-code/commit/1d7c205e8397983d3d79e59704db3f67a0c72937) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix the CLI exiting unexpectedly when reading an image from the clipboard fails; it now falls back to pasting text. + +- [#1753](https://github.com/MoonshotAI/kimi-code/pull/1753) [`d8ddabb`](https://github.com/MoonshotAI/kimi-code/commit/d8ddabb605c1f6fdcfa9fade8cc09b5f8c93651f) Thanks [@sailist](https://github.com/sailist)! - Fix the web server bearer-token check being bypassed by percent-encoded API paths (e.g. `/%61pi/v1/…`), which allowed unauthenticated access to every API route. + +- [#1758](https://github.com/MoonshotAI/kimi-code/pull/1758) [`1d7c205`](https://github.com/MoonshotAI/kimi-code/commit/1d7c205e8397983d3d79e59704db3f67a0c72937) Thanks [@RealKai42](https://github.com/RealKai42)! - Report crash telemetry for unhandled promise rejections, so exits they cause are no longer invisible. + +- [#1753](https://github.com/MoonshotAI/kimi-code/pull/1753) [`d8ddabb`](https://github.com/MoonshotAI/kimi-code/commit/d8ddabb605c1f6fdcfa9fade8cc09b5f8c93651f) Thanks [@sailist](https://github.com/sailist)! - Fix the session filesystem API following symlinks that point outside the workspace, which allowed reading, listing, creating, and downloading host files beyond the session directory through a planted symlink. + +- [#1753](https://github.com/MoonshotAI/kimi-code/pull/1753) [`d8ddabb`](https://github.com/MoonshotAI/kimi-code/commit/d8ddabb605c1f6fdcfa9fade8cc09b5f8c93651f) Thanks [@sailist](https://github.com/sailist)! - Fix sessions failing to be created when the workspace directory is given through a symlink, which the v2 engine rejected as "not a directory". + +- [#1754](https://github.com/MoonshotAI/kimi-code/pull/1754) [`1186686`](https://github.com/MoonshotAI/kimi-code/commit/11866865544b8ec88330372b8582e97a35113308) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix completed background subagents losing their final output after a session reload, and retry the output backfill when a transient fetch failure occurs. + +- [#1755](https://github.com/MoonshotAI/kimi-code/pull/1755) [`4f99114`](https://github.com/MoonshotAI/kimi-code/commit/4f99114342da11ebf7a403e3af6e0cf2c8cca431) Thanks [@kermanx](https://github.com/kermanx)! - Move the server's v1 wire schema definitions into the engine domains and the server package, removing the shared schema package from the v2 server stack with no behavior change. + +- [#1731](https://github.com/MoonshotAI/kimi-code/pull/1731) [`0b790cd`](https://github.com/MoonshotAI/kimi-code/commit/0b790cdc056475593abd572f657d010504caf752) Thanks [@sailist](https://github.com/sailist)! - web: Show every attachment a user sends — files, images, and videos — as chips in the message bubble, and let files be attached by dropping them anywhere in the window. + +- [#1744](https://github.com/MoonshotAI/kimi-code/pull/1744) [`b89d385`](https://github.com/MoonshotAI/kimi-code/commit/b89d385fa56915f067d656160086bc3c3126f8a3) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix Enter not confirming modal confirmation dialogs in dev builds, and keep the dialog open with a loading state until the confirmed action (such as archiving a session) completes. + +- [#1756](https://github.com/MoonshotAI/kimi-code/pull/1756) [`e885aec`](https://github.com/MoonshotAI/kimi-code/commit/e885aec7ffa9ee62d122908b68837c5e010d5d04) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show full diagnostics for model request failures — a semantic title, the provider's raw message, and expandable details (error code, HTTP status, request ID) with copy support — instead of a bare "Connection error" toast. + +- [#1751](https://github.com/MoonshotAI/kimi-code/pull/1751) [`df75a0f`](https://github.com/MoonshotAI/kimi-code/commit/df75a0f5c2f2e2dd3291c8adaba96a832ee1f179) Thanks [@kermanx](https://github.com/kermanx)! - web: Keep session activity indicators in sync with agent work, prevent duplicate streamed content after session activation races or LLM retries, and flush durable session events promptly. + +- [#1754](https://github.com/MoonshotAI/kimi-code/pull/1754) [`1186686`](https://github.com/MoonshotAI/kimi-code/commit/11866865544b8ec88330372b8582e97a35113308) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix a background subagent showing up as two identical rows in the agents dock panel during streaming. + +## 0.24.2 + +### Patch Changes + +- [#1704](https://github.com/MoonshotAI/kimi-code/pull/1704) [`38a2363`](https://github.com/MoonshotAI/kimi-code/commit/38a2363a006d8ed32ff6100ccff2dc7d1a70b2b0) Thanks [@sailist](https://github.com/sailist)! - Align the print-mode run lifecycle across engines: `print_background_mode` and `print_max_turns` now take effect for `kimi -p` on the experimental engine, with the same exit / drain / steer semantics and defaults as the default engine, and `kimi -p "/goal ..."` now stays alive until the goal reaches a terminal state instead of exiting after the first turn. + +- [#1704](https://github.com/MoonshotAI/kimi-code/pull/1704) [`38a2363`](https://github.com/MoonshotAI/kimi-code/commit/38a2363a006d8ed32ff6100ccff2dc7d1a70b2b0) Thanks [@sailist](https://github.com/sailist)! - Align the subagent timeout across engines: a fixed 2-hour default, overridable with `[subagent] timeout_ms` in config.toml or the KIMI_SUBAGENT_TIMEOUT_MS environment variable. + +- [#1727](https://github.com/MoonshotAI/kimi-code/pull/1727) [`286d3e7`](https://github.com/MoonshotAI/kimi-code/commit/286d3e7aca40a778cc4136eb377e14f14c70141c) Thanks [@liruifengv](https://github.com/liruifengv)! - Add a builtin `check-kimi-code-docs` skill that answers Kimi Code product questions (CLI usage, configuration, membership, error codes) against the official documentation with source links. It triggers automatically on product questions, or run `/check-kimi-code-docs`. + +- [#1707](https://github.com/MoonshotAI/kimi-code/pull/1707) [`8490c3e`](https://github.com/MoonshotAI/kimi-code/commit/8490c3e36b6a6cc3ba5c0f15d93b87347ce23878) Thanks [@sailist](https://github.com/sailist)! - Add the number of messages dropped during compaction retries to the session wire log's LLM request traces. + +- [#1740](https://github.com/MoonshotAI/kimi-code/pull/1740) [`a74ab44`](https://github.com/MoonshotAI/kimi-code/commit/a74ab44ac7d5656e2dd9cf93b8e484936b05a0c8) Thanks [@sailist](https://github.com/sailist)! - Increase the default per-step LLM retry budget from 3 to 10 attempts, so transient provider failures (429 / overload) are retried with exponential backoff for a few minutes before the turn fails. Tune with `loop_control.max_retries_per_step` in config.toml. + +- [#1707](https://github.com/MoonshotAI/kimi-code/pull/1707) [`8490c3e`](https://github.com/MoonshotAI/kimi-code/commit/8490c3e36b6a6cc3ba5c0f15d93b87347ce23878) Thanks [@sailist](https://github.com/sailist)! - Rename the dynamic tool loading model capability from `select_tools` to `dynamically_loaded_tools`, matching the model catalog vocabulary; the `select_tools` tool and the `tool-select` flag are unchanged. + +- [#1698](https://github.com/MoonshotAI/kimi-code/pull/1698) [`722694a`](https://github.com/MoonshotAI/kimi-code/commit/722694adf99c53dc608d417ea6d8c90a5712c33f) Thanks [@chengluyu](https://github.com/chengluyu)! - Enforce goal wall-clock budgets while model or tool work is still running. + +- [#1730](https://github.com/MoonshotAI/kimi-code/pull/1730) [`72f425e`](https://github.com/MoonshotAI/kimi-code/commit/72f425e18d0264010e1442af67ee8d9acf5f0659) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix tool call id collisions across turns for Gemini-protocol models, which merged separate swarm runs into a single card in the web UI. + +- [#1695](https://github.com/MoonshotAI/kimi-code/pull/1695) [`5c0f17c`](https://github.com/MoonshotAI/kimi-code/commit/5c0f17cfcf99c27eb697be11ae9b61243d993e4a) Thanks [@chengluyu](https://github.com/chengluyu)! - Preserve active goal elapsed time across crash recovery. + +- [#1743](https://github.com/MoonshotAI/kimi-code/pull/1743) [`481b28b`](https://github.com/MoonshotAI/kimi-code/commit/481b28b8f4d527c43c640c4d742c52aa006c3bb0) Thanks [@chengluyu](https://github.com/chengluyu)! - Correct the guidance text shown when a goal cannot be paused or resumed. + +- [#1692](https://github.com/MoonshotAI/kimi-code/pull/1692) [`e53cd79`](https://github.com/MoonshotAI/kimi-code/commit/e53cd799572db6b2c73f6938703d586b83013cec) Thanks [@chengluyu](https://github.com/chengluyu)! - Allow goals to use every configured turn before the turn budget stops further work. + +- [#1719](https://github.com/MoonshotAI/kimi-code/pull/1719) [`b24a347`](https://github.com/MoonshotAI/kimi-code/commit/b24a347e20a3efa7bba948316784a76439ed7cf5) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Restore the AgentSwarm member list after a page refresh on the v2 backend. + +- [#1704](https://github.com/MoonshotAI/kimi-code/pull/1704) [`38a2363`](https://github.com/MoonshotAI/kimi-code/commit/38a2363a006d8ed32ff6100ccff2dc7d1a70b2b0) Thanks [@sailist](https://github.com/sailist)! - Fix sessions created by newer builds failing to open in older CLI builds on the same machine; new sessions are written in a compatible layout, and existing sessions are healed on first open. + +- [#1708](https://github.com/MoonshotAI/kimi-code/pull/1708) [`ddfdfb0`](https://github.com/MoonshotAI/kimi-code/commit/ddfdfb0b09b59d95888eca7e9ddb7bb63be5e204) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix sub-agent completions being signaled as session turn completions, which fired premature completion notifications, sounds, and unread markers while the main turn was still running. + +- [#1714](https://github.com/MoonshotAI/kimi-code/pull/1714) [`20b6972`](https://github.com/MoonshotAI/kimi-code/commit/20b69724aafc8fb0b56a414988eb762a8b8a3ed1) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix code block copy buttons when the web UI is served over plain HTTP. + +- [#1643](https://github.com/MoonshotAI/kimi-code/pull/1643) [`d8d4e8c`](https://github.com/MoonshotAI/kimi-code/commit/d8d4e8ceb55d7a5cae7ce9b579996c9ff5601914) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Prevent long streaming responses from stalling after a tab is backgrounded. + +- [#1715](https://github.com/MoonshotAI/kimi-code/pull/1715) [`de493ae`](https://github.com/MoonshotAI/kimi-code/commit/de493aeec973623bc0e258d6598f6d9215693a5f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Use an upward chevron for the expand button on minimized plan review and question cards so the icon matches the direction the cards open. + +- [#1641](https://github.com/MoonshotAI/kimi-code/pull/1641) [`b6ae0a1`](https://github.com/MoonshotAI/kimi-code/commit/b6ae0a1054635fc71efde61dafa03da8a8b0c4c8) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show session list loading failures without discarding sessions that are still available. + +- [#1719](https://github.com/MoonshotAI/kimi-code/pull/1719) [`b24a347`](https://github.com/MoonshotAI/kimi-code/commit/b24a347e20a3efa7bba948316784a76439ed7cf5) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Expand the AgentSwarm card by default while its subagents are still running. + +- [#1693](https://github.com/MoonshotAI/kimi-code/pull/1693) [`7de218a`](https://github.com/MoonshotAI/kimi-code/commit/7de218a909d8f3e676ea3c160834090c9f19ca54) Thanks [@chengluyu](https://github.com/chengluyu)! - web: Resume paused goals when you select Resume. + +- [#1700](https://github.com/MoonshotAI/kimi-code/pull/1700) [`3107f96`](https://github.com/MoonshotAI/kimi-code/commit/3107f963a532de88d0affd0a08c60749455c5013) Thanks [@chengluyu](https://github.com/chengluyu)! - Prevent late activity from replaced goals from changing or consuming the budget of replacement goals. + +- [#1459](https://github.com/MoonshotAI/kimi-code/pull/1459) [`6eb8e13`](https://github.com/MoonshotAI/kimi-code/commit/6eb8e13417f28a553b4183f113e5b96eb31e4211) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix mobile safe-area handling, including the composer floating above the on-screen keyboard on iOS, doubled landscape insets, the PWA top bar under the notch, and toasts overlapping the composer as it grows. + +- [#1696](https://github.com/MoonshotAI/kimi-code/pull/1696) [`b781e8c`](https://github.com/MoonshotAI/kimi-code/commit/b781e8cbcfac2cd0e73e3b1b79fa3386c632fa5b) Thanks [@chengluyu](https://github.com/chengluyu)! - Preserve final status messages when automatic goal continuations reach a budget or report a blocker. + +- [#1722](https://github.com/MoonshotAI/kimi-code/pull/1722) [`3703d03`](https://github.com/MoonshotAI/kimi-code/commit/3703d0346e79e42f18b5097f5606e6ef7b0ff2dd) Thanks [@sailist](https://github.com/sailist)! - In print mode (`kimi -p`), keep the run alive by default while background tasks are pending and feed each completion back to the main agent as a new turn, with an effectively unbounded wait ceiling and turn cap and a 72-hour subagent timeout. Set `print_background_mode = "exit"` (or `"drain"`) to restore the previous exit-after-one-turn behavior. + +- [#1737](https://github.com/MoonshotAI/kimi-code/pull/1737) [`5d6ff02`](https://github.com/MoonshotAI/kimi-code/commit/5d6ff022b1a3732cf0b12d1a87497870def52c0c) Thanks [@sailist](https://github.com/sailist)! - In print mode (`kimi -p`), background Bash tasks and subagents no longer have a timeout by default — they run until they finish or the model stops them, and a foreground Bash command that times out is moved to the background without a new deadline. Interactive defaults are unchanged; tune per mode with `bash_task_timeout_s` under `[background]` or `timeout_ms` under `[subagent]` (`0` = no timeout). + +- [#1697](https://github.com/MoonshotAI/kimi-code/pull/1697) [`2bf009f`](https://github.com/MoonshotAI/kimi-code/commit/2bf009fe27d1b0259e90f285e94264a8bf6b5832) Thanks [@chengluyu](https://github.com/chengluyu)! - Reject subagent goal requests consistently instead of starting goals they cannot finish. + +- [#1711](https://github.com/MoonshotAI/kimi-code/pull/1711) [`9eff230`](https://github.com/MoonshotAI/kimi-code/commit/9eff230f976c6bd8cc757678293276d8dec013d8) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Log failed requests, WebSocket auth rejections, shutdowns, and key operations (abort, cancel, approvals, config changes) in the web UI server so daemon problems can be diagnosed from its logs. + +- [#1704](https://github.com/MoonshotAI/kimi-code/pull/1704) [`38a2363`](https://github.com/MoonshotAI/kimi-code/commit/38a2363a006d8ed32ff6100ccff2dc7d1a70b2b0) Thanks [@sailist](https://github.com/sailist)! - Fix `kimi server` reporting the internal server package version instead of the CLI version in its metadata; the web UI settings now show the CLI version. + +- [#1741](https://github.com/MoonshotAI/kimi-code/pull/1741) [`8a3f1ff`](https://github.com/MoonshotAI/kimi-code/commit/8a3f1ffa6fbd7855fd0b10d96587afc6b690ebe3) Thanks [@chengluyu](https://github.com/chengluyu)! - web: Fix the session title not being generated when the first message is a skill slash command. + +- [#1694](https://github.com/MoonshotAI/kimi-code/pull/1694) [`513f374`](https://github.com/MoonshotAI/kimi-code/commit/513f374aa08bd86b428f62697c1ca12594d533e9) Thanks [@chengluyu](https://github.com/chengluyu)! - Reject malformed persisted goal records during session recovery. + +- [#1704](https://github.com/MoonshotAI/kimi-code/pull/1704) [`38a2363`](https://github.com/MoonshotAI/kimi-code/commit/38a2363a006d8ed32ff6100ccff2dc7d1a70b2b0) Thanks [@sailist](https://github.com/sailist)! - web: Show each message's actual send time in chat history after reloading a session, instead of the session creation time. + +- [#1711](https://github.com/MoonshotAI/kimi-code/pull/1711) [`9eff230`](https://github.com/MoonshotAI/kimi-code/commit/9eff230f976c6bd8cc757678293276d8dec013d8) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Surface server error details when actions such as stopping a session, archiving, or toggling modes fail, instead of failing silently, and log every operation failure to the console and the exported web log. + +- [#1701](https://github.com/MoonshotAI/kimi-code/pull/1701) [`07c3632`](https://github.com/MoonshotAI/kimi-code/commit/07c3632415fa77972c49c39d7171ee5a4790bd01) Thanks [@sailist](https://github.com/sailist)! - Keep the workspace catalog complete and durable: creating a session registers its directory as a workspace, the server backfills missing workspaces from session history at startup, and a removed workspace no longer reappears after a restart. + +## 0.24.1 + +### Patch Changes + +- [#1678](https://github.com/MoonshotAI/kimi-code/pull/1678) [`ec1c974`](https://github.com/MoonshotAI/kimi-code/commit/ec1c9748c816d152bf06af2456e82ac35786bba9) Thanks [@chengluyu](https://github.com/chengluyu)! - Preserve goal completion summaries and show untyped LLM errors without an internal error-code prefix in step interruption events. + +- [#1688](https://github.com/MoonshotAI/kimi-code/pull/1688) [`94c0ef8`](https://github.com/MoonshotAI/kimi-code/commit/94c0ef89d29ea8532be02828201328fa1281273c) Thanks [@sailist](https://github.com/sailist)! - Fix built-in tools being unavailable when the model provider becomes ready after the session starts. + +- [#1684](https://github.com/MoonshotAI/kimi-code/pull/1684) [`e417ee7`](https://github.com/MoonshotAI/kimi-code/commit/e417ee7c2c282f00113dc0e4f4514ca5018b76c9) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix Kimi sessions getting stuck when preserved-thinking history contains an empty reasoning step. + +- [#1673](https://github.com/MoonshotAI/kimi-code/pull/1673) [`0f64b4d`](https://github.com/MoonshotAI/kimi-code/commit/0f64b4dcc4f2d295d0039b176d96d8003cb49991) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Align thinking-level handling with the CLI: submit the selected level verbatim instead of silently downgrading it, pin the model's catalog default when nothing was chosen, pre-select the target model's default on model switches, and persist explicit picks as the daemon-wide default so new sessions inherit them. + +- [#1689](https://github.com/MoonshotAI/kimi-code/pull/1689) [`ab22a2a`](https://github.com/MoonshotAI/kimi-code/commit/ab22a2adf0ca17cbb94f1abdab334ebc58814e8d) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show just the level name (e.g. Max) in the model pill instead of "thinking: max". + +- [#1625](https://github.com/MoonshotAI/kimi-code/pull/1625) [`d158e0a`](https://github.com/MoonshotAI/kimi-code/commit/d158e0a7ac4e432046d56787263dd2dbac40285e) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix Thinking effort routing so non-Kimi providers preserve configured values for upstream validation, while Kimi models validate runtime selections, fall back safely during model resolution, and synchronize the effective effort back to clients. + +## 0.24.0 + +### Minor Changes + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Add v2 session export support for packaging diagnostic zip archives. + +- [#1591](https://github.com/MoonshotAI/kimi-code/pull/1591) [`83e1753`](https://github.com/MoonshotAI/kimi-code/commit/83e175399f4dc3dfc3bb478543ff5897a24dfa3d) Thanks [@liruifengv](https://github.com/liruifengv)! - Move foreground Bash commands that hit their timeout to the background instead of killing them, so long-running commands survive the timeout and report back on completion. Set `bash_auto_background_on_timeout = false` under `[background]` in config.toml to restore the kill-on-timeout behavior. + +- [#1617](https://github.com/MoonshotAI/kimi-code/pull/1617) [`4ec2e7f`](https://github.com/MoonshotAI/kimi-code/commit/4ec2e7fab14ab89cddf77821082c3ff4911f737b) Thanks [@sailist](https://github.com/sailist)! - Run the local server (`kimi server run` / `kimi web`) on the agent-core-v2 engine by default — the `KIMI_CODE_EXPERIMENTAL_FLAG` opt-in is no longer needed, and the legacy v1 server package has been removed. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Port progressive tool disclosure to the new agent engine: MCP tool schemas stay out of the top-level tool list, and the model loads them by name on demand through the announcements plus the select_tools tool, keeping the prompt cache stable. Off by default; set KIMI_CODE_EXPERIMENTAL_TOOL_SELECT=1 to enable. + +- [#1646](https://github.com/MoonshotAI/kimi-code/pull/1646) [`5eb6217`](https://github.com/MoonshotAI/kimi-code/commit/5eb62178b3b67d8659788bdf91132469f6588653) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add session diagnostic export to download a session and bounded metadata-only troubleshooting logs as a ZIP. Run `/export` or pick Export session from a session's more menu. Web downloads are limited to 64 MiB. + +### Patch Changes + +- [#1638](https://github.com/MoonshotAI/kimi-code/pull/1638) [`7c889f3`](https://github.com/MoonshotAI/kimi-code/commit/7c889f3a960482cc9382203bda55d972b6fb6acd) Thanks [@RealKai42](https://github.com/RealKai42)! - In auto permission mode, plan exits are now marked as auto-approved (not user-reviewed) in both the tool result and the transcript, so the agent no longer treats automatic plan approval as a user signal to start executing. + +- [#1598](https://github.com/MoonshotAI/kimi-code/pull/1598) [`4feca6b`](https://github.com/MoonshotAI/kimi-code/commit/4feca6b0738ee0120ab8bea04604b8f467a72e48) Thanks [@kermanx](https://github.com/kermanx)! - web: Recover transient subagent rate limits without surfacing them as session errors. + +- [#1635](https://github.com/MoonshotAI/kimi-code/pull/1635) [`e49b3b8`](https://github.com/MoonshotAI/kimi-code/commit/e49b3b877750ba5ca0ea80e154549d5b53455575) Thanks [@sailist](https://github.com/sailist)! - Request task-owned work to stop on session close, honoring `background.keep_alive_on_exit` for independent processes and `background.kill_grace_period_ms` before attempting force-stop. + +- [#1629](https://github.com/MoonshotAI/kimi-code/pull/1629) [`0527ca2`](https://github.com/MoonshotAI/kimi-code/commit/0527ca2267f8cf355d0c158953f3dbfc0c9692ac) Thanks [@sailist](https://github.com/sailist)! - Fix session fork losing everything except the conversation log: forked sessions now carry over media attachments, plan files, background task output, and cron tasks, and a failed fork no longer leaves a broken half-copy behind. + +- [#1627](https://github.com/MoonshotAI/kimi-code/pull/1627) [`28e9dd4`](https://github.com/MoonshotAI/kimi-code/commit/28e9dd4d627f01143b715976cb071e7d16cd2001) Thanks [@chengluyu](https://github.com/chengluyu)! - web: Continue blocked goals after the user resumes them from the goal controls. + +- [#1631](https://github.com/MoonshotAI/kimi-code/pull/1631) [`2d874fb`](https://github.com/MoonshotAI/kimi-code/commit/2d874fbd73eb511e4ef4c8d4c88bd47e429580b2) Thanks [@sailist](https://github.com/sailist)! - Fix a race where a heartbeat write in flight during server shutdown could recreate the instance file right after it was removed. + +- [#1663](https://github.com/MoonshotAI/kimi-code/pull/1663) [`1294a0e`](https://github.com/MoonshotAI/kimi-code/commit/1294a0e1ad739151573163505f9c58afb2d543e4) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix OAuth login hanging after browser authorization when the provider configuration changes during sign-in. + +- [#1657](https://github.com/MoonshotAI/kimi-code/pull/1657) [`32a89c3`](https://github.com/MoonshotAI/kimi-code/commit/32a89c36432f9aea452a697734102a7956e42e92) Thanks [@RealKai42](https://github.com/RealKai42)! - Prevent oversized image reads from poisoning sessions and recover existing request-too-large failures by removing unsafe media from provider requests. + +- [#1635](https://github.com/MoonshotAI/kimi-code/pull/1635) [`e49b3b8`](https://github.com/MoonshotAI/kimi-code/commit/e49b3b877750ba5ca0ea80e154549d5b53455575) Thanks [@sailist](https://github.com/sailist)! - Store background task records per agent again, so tasks written by older versions are found on resume and one agent's restore no longer marks another agent's tasks as lost. + +- [#1632](https://github.com/MoonshotAI/kimi-code/pull/1632) [`a4aae87`](https://github.com/MoonshotAI/kimi-code/commit/a4aae87cd9a240d3567601ed1a9aefaab540b075) Thanks [@sailist](https://github.com/sailist)! - Fix providers without a configured base_url being rejected: anthropic/openai and other protocol providers now fall back to their official default endpoints again, as before. + +- [#1588](https://github.com/MoonshotAI/kimi-code/pull/1588) [`2061590`](https://github.com/MoonshotAI/kimi-code/commit/20615902c2c3776d17c6c334cedec1c8723222b1) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix pasted media being dropped from /skill and plugin command arguments. + +- [#1588](https://github.com/MoonshotAI/kimi-code/pull/1588) [`2061590`](https://github.com/MoonshotAI/kimi-code/commit/20615902c2c3776d17c6c334cedec1c8723222b1) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix pasted images being dropped when steering with Ctrl-S. + +- [#1629](https://github.com/MoonshotAI/kimi-code/pull/1629) [`0527ca2`](https://github.com/MoonshotAI/kimi-code/commit/0527ca2267f8cf355d0c158953f3dbfc0c9692ac) Thanks [@sailist](https://github.com/sailist)! - Fix the v2 engine never activating tool-call deduplication: identical tool calls issued in the same step no longer execute multiple times, and repeated identical calls across steps receive escalating reminders again. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Fix a race in the experimental v2 config service that could drop a just-written setting from the config response. + +- [#1614](https://github.com/MoonshotAI/kimi-code/pull/1614) [`3c0e368`](https://github.com/MoonshotAI/kimi-code/commit/3c0e368cbdfebff9632cffca3b18365615a146b8) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix a server crash when the first goal-mode prompt is submitted while the v2 agent is still starting. + +- [#1631](https://github.com/MoonshotAI/kimi-code/pull/1631) [`2d874fb`](https://github.com/MoonshotAI/kimi-code/commit/2d874fbd73eb511e4ef4c8d4c88bd47e429580b2) Thanks [@sailist](https://github.com/sailist)! - Surface the provider's actual rejection message instead of a misleading re-login prompt when an OAuth-managed model keeps returning 401 after a token refresh. + +- [#1631](https://github.com/MoonshotAI/kimi-code/pull/1631) [`2d874fb`](https://github.com/MoonshotAI/kimi-code/commit/2d874fbd73eb511e4ef4c8d4c88bd47e429580b2) Thanks [@sailist](https://github.com/sailist)! - Rewrite repeated-tool-call reminders to redirect the agent toward a different action instead of prohibiting the call, and treat a dismissed question prompt as no answer rather than the recommended option. + +- [#1636](https://github.com/MoonshotAI/kimi-code/pull/1636) [`8027fe2`](https://github.com/MoonshotAI/kimi-code/commit/8027fe291b03fbfce6dc60aa06f8699ad0976ec5) Thanks [@sailist](https://github.com/sailist)! - Make file tools able to reach skill directories outside the working directory in the v2 engine (experimental), and honor --skillsDir in v2 print mode and the server's skillDirs option. + +- [#1630](https://github.com/MoonshotAI/kimi-code/pull/1630) [`0303b82`](https://github.com/MoonshotAI/kimi-code/commit/0303b82c3e691836163ecf906febfb6324c81d74) Thanks [@sailist](https://github.com/sailist)! - Fix ReadMediaFile results losing their image rendering after a session reload or resume on the v2 server backend. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Fix a storage race in the experimental v2 engine that could fail value reads when writes overlap with compaction. + +- [#1601](https://github.com/MoonshotAI/kimi-code/pull/1601) [`dc309a7`](https://github.com/MoonshotAI/kimi-code/commit/dc309a7dfb38b6ef885b8ae80be51b49f8486207) Thanks [@kermanx](https://github.com/kermanx)! - web: Fix the context usage indicator dropping to 0 when a session is reopened or the session list reloads (e.g. after a sidebar search) — the cached live usage is now kept instead of the session record's all-zero placeholder. + +- [#1620](https://github.com/MoonshotAI/kimi-code/pull/1620) [`e91a616`](https://github.com/MoonshotAI/kimi-code/commit/e91a616f2196ab9ffc69b3fcc0f2015398d86bd4) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix duplicate user message bubbles after a session snapshot resync. + +- [#1672](https://github.com/MoonshotAI/kimi-code/pull/1672) [`88629ba`](https://github.com/MoonshotAI/kimi-code/commit/88629bac3add2a8a17ae8288ee4edbdc9313d55a) Thanks [@yicun](https://github.com/yicun)! - web: Fix uploaded and persisted images failing to display on non-loopback server connections. + +- [#1609](https://github.com/MoonshotAI/kimi-code/pull/1609) [`e223549`](https://github.com/MoonshotAI/kimi-code/commit/e223549a79c80e442850947c0cf60d58b2d18667) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix a running multi-step turn rendering a duplicated wall of text after the page reconnects or refreshes mid-turn. + +- [#1611](https://github.com/MoonshotAI/kimi-code/pull/1611) [`32cbd0c`](https://github.com/MoonshotAI/kimi-code/commit/32cbd0cf6109f4f3f124e6e4ee7c4c87fa344247) Thanks [@chengluyu](https://github.com/chengluyu)! - web: Fix the workspace picker menu sizing too narrowly for its content. + +- [#1635](https://github.com/MoonshotAI/kimi-code/pull/1635) [`e49b3b8`](https://github.com/MoonshotAI/kimi-code/commit/e49b3b877750ba5ca0ea80e154549d5b53455575) Thanks [@sailist](https://github.com/sailist)! - Fix possible record loss when resuming sessions whose wire log needs migration, and reject session logs missing the version envelope instead of silently misreading them. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Fix MCP tools being unavailable on the first turn after session startup. + +- [#1580](https://github.com/MoonshotAI/kimi-code/pull/1580) [`83370f1`](https://github.com/MoonshotAI/kimi-code/commit/83370f17ef38770561a421e3b3a15f6244219aa5) Thanks [@wszqkzqk](https://github.com/wszqkzqk)! - Fix bash auto-detection on Windows failing when git comes from a native MSYS2 toolchain (ucrt64/clang64/clangarm64). + +- [#1676](https://github.com/MoonshotAI/kimi-code/pull/1676) [`d1820ff`](https://github.com/MoonshotAI/kimi-code/commit/d1820ff0f853689e84b3e9d4c482532c481eb9bd) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve empty model reasoning blocks across providers so multi-step tool calls can continue. + +- [#1669](https://github.com/MoonshotAI/kimi-code/pull/1669) [`490303d`](https://github.com/MoonshotAI/kimi-code/commit/490303db16ed374eae20572e4c6f9880db911547) Thanks [@chengluyu](https://github.com/chengluyu)! - web: Refine goal mode controls with animated strip interactions, budget-aware progress, and design-system cancellation confirmation. + +- [#1597](https://github.com/MoonshotAI/kimi-code/pull/1597) [`d601847`](https://github.com/MoonshotAI/kimi-code/commit/d601847f22366b041d949d7c9f7857471be8970c) Thanks [@7Sageer](https://github.com/7Sageer)! - Send the kimi-code-cli User-Agent on provider registry (api.json) and model catalog fetches, so registries can identify the client version. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Fix approval and question prompts not appearing in real time for web clients connected to the v2 server; they previously only showed up after a page refresh. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Log a warning when a skill fails to parse instead of silently dropping it, and fix the skill catalog so scanned skill roots and policy-skipped skills are actually reported. + +- [#1589](https://github.com/MoonshotAI/kimi-code/pull/1589) [`f338fcd`](https://github.com/MoonshotAI/kimi-code/commit/f338fcdac4fa8d4235c44310953e5d512f6549fb) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the AgentSwarm member list disappearing after a page refresh while subagents are still running. + +- [#1591](https://github.com/MoonshotAI/kimi-code/pull/1591) [`83e1753`](https://github.com/MoonshotAI/kimi-code/commit/83e175399f4dc3dfc3bb478543ff5897a24dfa3d) Thanks [@liruifengv](https://github.com/liruifengv)! - Optimize the TaskOutput tool prompts to discourage blocking waits on background tasks. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Enforce a typed registry for v2 engine telemetry events and redact URLs, tokens, and file paths from outgoing telemetry properties. + +- [#1624](https://github.com/MoonshotAI/kimi-code/pull/1624) [`3215129`](https://github.com/MoonshotAI/kimi-code/commit/321512986099037acb4b2677d4455db316d27b50) Thanks [@kermanx](https://github.com/kermanx)! - Fix the experimental v2 engine crashing when the first prompt is sent right after a new conversation is created (for example sending /goal on the web's new-conversation page): agent creation now joins the in-flight bootstrap instead of failing, and the v2 agent lifecycle is split into focused existence, sub-agent, and session MCP domains. + +- [#1637](https://github.com/MoonshotAI/kimi-code/pull/1637) [`0e0a6e9`](https://github.com/MoonshotAI/kimi-code/commit/0e0a6e9a5170c28c5e6809c1b2cf6d6f8904de73) Thanks [@sailist](https://github.com/sailist)! - Support caller-supplied MCP server configs on session create in the v2 engine (experimental), merged over the file config and under plugin servers. + +- [#1626](https://github.com/MoonshotAI/kimi-code/pull/1626) [`1c85f94`](https://github.com/MoonshotAI/kimi-code/commit/1c85f94472ead2746ad6860ec0e09f4384dd95ec) Thanks [@sailist](https://github.com/sailist)! - v2 engine: expose the prompt scheduler over /api/v2 for native clients, and add an experimental fault-injection service (KIMI_CODE_EXPERIMENTAL_FAULT_INJECTION) that arms a one-shot provider failure so the media-degraded / media-stripped recovery resends can be exercised end-to-end. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Introduce a graded error taxonomy for the v2 engine's filesystem, storage, and wire layers, translating raw OS and parse failures into specific error codes instead of generic internal errors. + +- [#1626](https://github.com/MoonshotAI/kimi-code/pull/1626) [`1c85f94`](https://github.com/MoonshotAI/kimi-code/commit/1c85f94472ead2746ad6860ec0e09f4384dd95ec) Thanks [@sailist](https://github.com/sailist)! - v2 engine: block unsupported image formats (AVIF, HEIC, BMP, TIFF, ICO) at every ingestion point so they can no longer poison session history, and auto-recover provider image-format rejections with a media-stripped resend. + +- [#1626](https://github.com/MoonshotAI/kimi-code/pull/1626) [`1c85f94`](https://github.com/MoonshotAI/kimi-code/commit/1c85f94472ead2746ad6860ec0e09f4384dd95ec) Thanks [@sailist](https://github.com/sailist)! - v2 engine: recover image-heavy sessions from provider request-size rejections (HTTP 413) by resending with older media degraded to text markers, re-encode oversized WebP images instead of passing them through, and keep downscaled PNGs readable by switching to JPEG below 1000px. + +- [#1613](https://github.com/MoonshotAI/kimi-code/pull/1613) [`b2daa40`](https://github.com/MoonshotAI/kimi-code/commit/b2daa405f075cb6847c0a313809b1bcac750b611) Thanks [@7Sageer](https://github.com/7Sageer)! - Support the `services.moonshot_search` api-key config for WebSearch in the v2 engine, matching v1: the tool is now available without an OAuth login, and explicit config takes precedence over the OAuth-derived provider. + +- [#1590](https://github.com/MoonshotAI/kimi-code/pull/1590) [`8a4ee05`](https://github.com/MoonshotAI/kimi-code/commit/8a4ee05951ebe4f804fd1fb0989aaf44b3b7a3ed) Thanks [@sailist](https://github.com/sailist)! - Fix bash auto-detection on Windows in the experimental v2 engine when git comes from a native MSYS2 toolchain (ucrt64/clang64/clangarm64). + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Send the CLI identity headers (User-Agent and device identity) with outbound requests from the experimental v2 server, matching direct CLI runs. + +- [#1593](https://github.com/MoonshotAI/kimi-code/pull/1593) [`2185237`](https://github.com/MoonshotAI/kimi-code/commit/2185237c2f5c5fb3cc6b44c01ac158c6e2b81fe6) Thanks [@kermanx](https://github.com/kermanx)! - Declare v2 engine wire op payloads with required zod schemas and derive their types from the schemas, with ops declared on their models and every op type registered for replay classification. + +- [#1441](https://github.com/MoonshotAI/kimi-code/pull/1441) [`ceb158d`](https://github.com/MoonshotAI/kimi-code/commit/ceb158dc54586f254819edbc83c27e21dca1ecf6) Thanks [@sailist](https://github.com/sailist)! - Keep sessions from the new agent engine compatible with existing transcript replay. + +- [#1592](https://github.com/MoonshotAI/kimi-code/pull/1592) [`924d5c9`](https://github.com/MoonshotAI/kimi-code/commit/924d5c914143d178020c2dddc56906ce15088680) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show the connected backend engine (v1 / v2) in Settings, and add a dev-mode backend pill next to the sidebar brand that can switch the dev proxy between the two engines at runtime. + +- [#1606](https://github.com/MoonshotAI/kimi-code/pull/1606) [`2da45fc`](https://github.com/MoonshotAI/kimi-code/commit/2da45fc419cf5285a9353df8690bba444037ffe4) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix the goal card disappearing after a page refresh while a session goal is active. + +- [#1587](https://github.com/MoonshotAI/kimi-code/pull/1587) [`49a8c84`](https://github.com/MoonshotAI/kimi-code/commit/49a8c84a493610c2b2cc2c7da0a8ec0261d876db) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Let wide Markdown tables in the desktop chat grow up to 1040px with each column capped at 700px so long cell content wraps, temporarily hiding the conversation outline while a table passes under it; anything wider still scrolls inside the table. + +## 0.23.6 + +### Patch Changes + +- [#1550](https://github.com/MoonshotAI/kimi-code/pull/1550) [`f17a6ec`](https://github.com/MoonshotAI/kimi-code/commit/f17a6ecb52907ffabf67a26de65df89572ac515a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Treat a dismissed question prompt as the user choosing not to answer, instead of implicitly selecting the recommended option. + +- [#1488](https://github.com/MoonshotAI/kimi-code/pull/1488) [`7bd29ab`](https://github.com/MoonshotAI/kimi-code/commit/7bd29ab0117a1c15691404f411fd67f511bbb897) Thanks [@starquakee](https://github.com/starquakee)! - Rename the dynamic tool loading model capability from `select_tools` to `dynamically_loaded_tools`. + +- [#1497](https://github.com/MoonshotAI/kimi-code/pull/1497) [`c6e02da`](https://github.com/MoonshotAI/kimi-code/commit/c6e02daf421b47e8451e60ed4d7b3847a895d00b) Thanks [@sailist](https://github.com/sailist)! - Add a print-mode background policy that lets `kimi -p` stay alive across background-task completions so the main agent can be steered into follow-up turns. Set `[background].print_background_mode = "steer"` to enable it. + +- [#1555](https://github.com/MoonshotAI/kimi-code/pull/1555) [`2f97917`](https://github.com/MoonshotAI/kimi-code/commit/2f97917bb5edc8bdb9837724e57a88f5c0e1f2bd) Thanks [@sailist](https://github.com/sailist)! - Keep `kimi -p` runs alive after a turn ends while a goal is still active or a cron task is pending, so goal continuations and cron fires run their turns instead of being cut off when the main turn finishes. + +- [#1564](https://github.com/MoonshotAI/kimi-code/pull/1564) [`cc03816`](https://github.com/MoonshotAI/kimi-code/commit/cc03816ee0a89b272c1ab87ca43ed246833f0453) Thanks [@sailist](https://github.com/sailist)! - Recognize the support_efforts and default_effort fields when importing a custom registry, so thinking effort levels are available for those models. + +- [#1562](https://github.com/MoonshotAI/kimi-code/pull/1562) [`faefad0`](https://github.com/MoonshotAI/kimi-code/commit/faefad0e290ceacb89851baa42043c8685b08dc9) Thanks [@sailist](https://github.com/sailist)! - Add a `subagent.timeout_ms` config option to control how long a single subagent may run before timing out, and raise the default from 30 minutes to 2 hours. Set `[subagent] timeout_ms` in config.toml (or the `KIMI_SUBAGENT_TIMEOUT_MS` env var) to adjust it. + +- [#1572](https://github.com/MoonshotAI/kimi-code/pull/1572) [`3a7aad6`](https://github.com/MoonshotAI/kimi-code/commit/3a7aad653f1226ce4e2c7103318471f65154c406) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix sessions getting stuck in a sending state after a reconnect, so the working spinner stops and the next message sends normally once a turn finishes while the connection is down. + +- [#1574](https://github.com/MoonshotAI/kimi-code/pull/1574) [`b1942bd`](https://github.com/MoonshotAI/kimi-code/commit/b1942bd5718c46991ba5021b4ae96dbf2458617c) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the first visit after starting or updating the web UI bouncing to the login page when the initial auth check fails; the connecting screen now stays up, shows the connection error, and retries until the server answers. + +- [#1553](https://github.com/MoonshotAI/kimi-code/pull/1553) [`264525e`](https://github.com/MoonshotAI/kimi-code/commit/264525eb51f87409a8961bcf3f5f0271ab767c49) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the chat view jumping downward while scrolling through conversation history. + +- [#1565](https://github.com/MoonshotAI/kimi-code/pull/1565) [`1d3dba5`](https://github.com/MoonshotAI/kimi-code/commit/1d3dba56832b69628f3bb22ce240f38a08f0af3a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the model dropdown showing checkmarks on same-named models from other providers; the current model is now matched by its unique model id. + +- [#1567](https://github.com/MoonshotAI/kimi-code/pull/1567) [`f901b9e`](https://github.com/MoonshotAI/kimi-code/commit/f901b9e1da9a9575b5d47dba40babe2ccd035180) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Keep the server access token for up to 7 days across tab close and browser restarts, instead of asking for it again with every new tab. + +- [#1552](https://github.com/MoonshotAI/kimi-code/pull/1552) [`37bb4b8`](https://github.com/MoonshotAI/kimi-code/commit/37bb4b870edf6a5458dda755a5b4a432c32df2a7) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix ReadMediaFile results rendering as plain tool cards instead of images after resuming or reloading a session. + +- [#1563](https://github.com/MoonshotAI/kimi-code/pull/1563) [`c982386`](https://github.com/MoonshotAI/kimi-code/commit/c98238699c1ae51a2237969b43282373fc0c0e89) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix sidebar lag with many sessions by removing repeated session list scans during rendering. + +- [#1475](https://github.com/MoonshotAI/kimi-code/pull/1475) [`5a208cb`](https://github.com/MoonshotAI/kimi-code/commit/5a208cb041530e320f343a46e231bf3c109e30c9) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Auto-enable the default thinking effort when switching to a model that supports effort levels in the web UI. + +- [#1577](https://github.com/MoonshotAI/kimi-code/pull/1577) [`6fc1deb`](https://github.com/MoonshotAI/kimi-code/commit/6fc1deb45312574a9e97ffaf6d7ced530d38910d) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Let wide Markdown tables in the desktop chat grow beyond the reading column up to 1040px, temporarily hiding the conversation outline while a table passes under it; anything wider still scrolls inside the table. + +- [#1575](https://github.com/MoonshotAI/kimi-code/pull/1575) [`9d96b53`](https://github.com/MoonshotAI/kimi-code/commit/9d96b538bf4311fdf07aa262a1b4141f7bdd83ed) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Let wide Markdown tables scroll horizontally inside the table instead of being squeezed into the reading column. + +- [#1556](https://github.com/MoonshotAI/kimi-code/pull/1556) [`d2c2c33`](https://github.com/MoonshotAI/kimi-code/commit/d2c2c33f3e89c7c9ed06aa7c2376b88b6107e41d) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add workspaces by typing an absolute path directly in the workspace picker's search box, with live validation and completion suggestions. + +- [#1547](https://github.com/MoonshotAI/kimi-code/pull/1547) [`19c5aa6`](https://github.com/MoonshotAI/kimi-code/commit/19c5aa64ebef86925ad58074ebcac6a5a7a8ff8d) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Update the WebBridge install page link opened from the /plugins panel. + +## 0.23.5 + +### Patch Changes + +- [#1542](https://github.com/MoonshotAI/kimi-code/pull/1542) [`f80b2ea`](https://github.com/MoonshotAI/kimi-code/commit/f80b2eaf04925ce920f693fc8d4d81cb00e825d7) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the "Turn finished" desktop notification and completion sound firing twice per turn. + +- [#1535](https://github.com/MoonshotAI/kimi-code/pull/1535) [`04041eb`](https://github.com/MoonshotAI/kimi-code/commit/04041eb998b6798898fa5df97f7587b3aa119b27) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Hide the internal image-compression note so it no longer renders as user message text. + +- [#1536](https://github.com/MoonshotAI/kimi-code/pull/1536) [`db61c9e`](https://github.com/MoonshotAI/kimi-code/commit/db61c9e2dddcb6c35b019ef7f374385248ece881) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop unsupported image formats (AVIF, BMP, TIFF, ICO, …) from breaking sessions at every entry point — including remote image URLs and images mislabeled by a tool — and recover an already-stuck session by dropping the offending image and retrying, so one such image can no longer make every later request fail. + +- [#1530](https://github.com/MoonshotAI/kimi-code/pull/1530) [`9f66ec4`](https://github.com/MoonshotAI/kimi-code/commit/9f66ec416cc658842dc1414b79b6447d1b4cc7f9) Thanks [@sailist](https://github.com/sailist)! - Retry provider 429, overload, and other transient errors more reliably, honoring the server Retry-After delay, and surface retries in `-p --output-format stream-json`. + +## 0.23.4 + +### Patch Changes + +- [#1501](https://github.com/MoonshotAI/kimi-code/pull/1501) [`b91099e`](https://github.com/MoonshotAI/kimi-code/commit/b91099ed7a2590d1afa4d6e3675671da52b7661c) Thanks [@liruifengv](https://github.com/liruifengv)! - Display Extra Usage (fuel pack) balance in `/usage` and `/status` commands. + +- [#1517](https://github.com/MoonshotAI/kimi-code/pull/1517) [`173bdfd`](https://github.com/MoonshotAI/kimi-code/commit/173bdfdab1f484ed79927aeaac7dc8116d3fd346) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix resuming sessions whose original working directory no longer exists. + +- [#1516](https://github.com/MoonshotAI/kimi-code/pull/1516) [`9fb1915`](https://github.com/MoonshotAI/kimi-code/commit/9fb19154accf6b6f7abfbf7a9820ccda517bc87e) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix prompt-mode goals so they run until completion and report invalid goal commands before sending prompts. + +- [#1508](https://github.com/MoonshotAI/kimi-code/pull/1508) [`1bf2c9a`](https://github.com/MoonshotAI/kimi-code/commit/1bf2c9afee4643fbf6755f0b92fd60aa14240501) Thanks [@RealKai42](https://github.com/RealKai42)! - Keep image-heavy sessions within provider request-size limits: model-read images now honor a 256 KB per-image budget and a 2000px downscale cap (configurable via `[image]` in config.toml or `KIMI_IMAGE_*` env vars), oversized WebP is compressed as well, HEIC/HEIF reads are refused with a platform-matched conversion command instead of poisoning the session, and a request-too-large rejection (HTTP 413) now recovers automatically — the request and /compact both retry with older media replaced by text markers instead of failing the session. + +- [#1519](https://github.com/MoonshotAI/kimi-code/pull/1519) [`170ae44`](https://github.com/MoonshotAI/kimi-code/commit/170ae4420526b6592d696cd597d1693dbd1a660b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Polish the session sidebar layout, colors, icons, and typography. + +- [#1521](https://github.com/MoonshotAI/kimi-code/pull/1521) [`046b6c4`](https://github.com/MoonshotAI/kimi-code/commit/046b6c417581792933732c7ffe154e120c96171d) Thanks [@RealKai42](https://github.com/RealKai42)! - The `[image]` limits in config.toml now also apply to pasted images (CLI paste and ACP prompts), and each core now uses its own settings, so reloading one client's config no longer changes another client's image compression. + +- [#1494](https://github.com/MoonshotAI/kimi-code/pull/1494) [`a354803`](https://github.com/MoonshotAI/kimi-code/commit/a3548035a8b6d25df9a11daab37a21daee1ef73f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add a Kimi WebBridge entry to the Official tab of the /plugins panel that opens the WebBridge install page in your browser. + +- [#1479](https://github.com/MoonshotAI/kimi-code/pull/1479) [`735922c`](https://github.com/MoonshotAI/kimi-code/commit/735922c291ec3d32d60da6af053f75e1c6179f92) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add notifications when a tool needs approval, and improve notification reliability. + +- [#1522](https://github.com/MoonshotAI/kimi-code/pull/1522) [`ec8dc34`](https://github.com/MoonshotAI/kimi-code/commit/ec8dc3456c1696a5eba6c37b6e26ef99837c35e2) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix an occasional "another turn is active" error when sending the first message of a new conversation, and show a starting state while it is being sent. + +- [#1502](https://github.com/MoonshotAI/kimi-code/pull/1502) [`ad30a1c`](https://github.com/MoonshotAI/kimi-code/commit/ad30a1c6328327729221f9f5fc700b621dfef779) Thanks [@chengluyu](https://github.com/chengluyu)! - web: Polish the chat UI with Inter typography, localized labels, and tighter sidebar, composer, and menu styling. + +## 0.23.3 + +### Patch Changes + +- [#1506](https://github.com/MoonshotAI/kimi-code/pull/1506) [`e83511a`](https://github.com/MoonshotAI/kimi-code/commit/e83511a7118652a67676bbcfd41148907ad7b8de) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix a misleading "OAuth login expired" message shown when a model is not available for the current account. + +## 0.23.2 + +### Patch Changes + +- [#1489](https://github.com/MoonshotAI/kimi-code/pull/1489) [`2206d21`](https://github.com/MoonshotAI/kimi-code/commit/2206d21327129aa2331b6b159cfce61110b7f94f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add the Vercel plugin to the bundled plugin marketplace. Run /plugins and select Vercel Plugin to install it. + +- [#1477](https://github.com/MoonshotAI/kimi-code/pull/1477) [`150206a`](https://github.com/MoonshotAI/kimi-code/commit/150206a6f7027879df954e26736b4baa5d336235) Thanks [@chengluyu](https://github.com/chengluyu)! - Count the turn that starts an autonomous goal toward its goal turn usage. + +- [#1483](https://github.com/MoonshotAI/kimi-code/pull/1483) [`f30781b`](https://github.com/MoonshotAI/kimi-code/commit/f30781bb273321f3e3bbb548a9d0724ab6299fc6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix `kimi -p` runs exiting with code 0 when a turn fails. + +- [#1466](https://github.com/MoonshotAI/kimi-code/pull/1466) [`063bce2`](https://github.com/MoonshotAI/kimi-code/commit/063bce2a2f52601abaa0d13173ab88371cbbe9ae) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix console windows flashing on Windows each time a hook runs. + +- [#1474](https://github.com/MoonshotAI/kimi-code/pull/1474) [`11c6a37`](https://github.com/MoonshotAI/kimi-code/commit/11c6a37ce030f8e64de5c810da07ec7fab3b0615) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the connection error toast lingering after the WebSocket reconnects when returning from the background. + +- [#1476](https://github.com/MoonshotAI/kimi-code/pull/1476) [`d1a964f`](https://github.com/MoonshotAI/kimi-code/commit/d1a964fba9b3dca902ea6f81bacaccc839955c03) Thanks [@chengluyu](https://github.com/chengluyu)! - Prevent autonomous goals from being paused by model-reported status updates. + +- [#1481](https://github.com/MoonshotAI/kimi-code/pull/1481) [`1317000`](https://github.com/MoonshotAI/kimi-code/commit/131700097a732b97b3d17c5e2efa1c5a44b013ef) Thanks [@chengluyu](https://github.com/chengluyu)! - Tighten goal-mode guidance for blocked and complete status updates. + +- [#1460](https://github.com/MoonshotAI/kimi-code/pull/1460) [`474ce28`](https://github.com/MoonshotAI/kimi-code/commit/474ce289dd39aa42d1a77a9a2e15531aee49aa15) Thanks [@RealKai42](https://github.com/RealKai42)! - Raise the image downscale cap from 2000px to 3000px, and fix swapped width/height for EXIF-rotated (portrait) photos in compression captions and media read notes so region readback coordinates map correctly. + +- [#1467](https://github.com/MoonshotAI/kimi-code/pull/1467) [`ee38545`](https://github.com/MoonshotAI/kimi-code/commit/ee385456d0eda380fec067db92c025462db13f5a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Compile icons at build time so the bundled web UI only carries the icons it renders. + +- [#1471](https://github.com/MoonshotAI/kimi-code/pull/1471) [`9b76e5b`](https://github.com/MoonshotAI/kimi-code/commit/9b76e5bff631cceaeecb2b0cbc096533c5fdc8cc) Thanks [@starquakee](https://github.com/starquakee)! - Progressive tool disclosure (`select_tools`, experimental): compaction now discards the loaded tool schemas instead of re-injecting them. After a compaction the boundary announcement re-lists every loadable tool name and the model re-selects what it still needs; a from-memory call to a no-longer-loaded tool is rejected with guidance to select it first. This keeps the post-compaction context at its minimal users+summary floor and removes the schema-rebuild budget heuristics. No effect unless the `tool-select` experimental flag and a `select_tools`-capable model are active. + +- [#1491](https://github.com/MoonshotAI/kimi-code/pull/1491) [`0cc9831`](https://github.com/MoonshotAI/kimi-code/commit/0cc9831a2f79d93903259bd3353e746abac01b67) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: The composer model switcher switches the active session's model as before and additionally bumps the global default model, so new sessions inherit the choice. + +- [#1490](https://github.com/MoonshotAI/kimi-code/pull/1490) [`b30a45e`](https://github.com/MoonshotAI/kimi-code/commit/b30a45efecfa5ece4f4f10f2c5403ba097e7690b) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Press Enter to confirm in archive and other confirmation dialogs. + +- [#1480](https://github.com/MoonshotAI/kimi-code/pull/1480) [`2ad0120`](https://github.com/MoonshotAI/kimi-code/commit/2ad0120c2a5c8383892e4da1ee7c6853926ed365) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Redesign the scheduled reminder UI. + +- [#1492](https://github.com/MoonshotAI/kimi-code/pull/1492) [`b0809dd`](https://github.com/MoonshotAI/kimi-code/commit/b0809ddac833d8d920d95187f7ef64f97bafdbc6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show session skills in the slash menu as `/skill:<name>` so they are distinguishable from built-in commands; typing the bare skill name still works. + +## 0.23.1 + +### Patch Changes + +- [#1432](https://github.com/MoonshotAI/kimi-code/pull/1432) [`25a655c`](https://github.com/MoonshotAI/kimi-code/commit/25a655cf88b2f5861f9c0b7ea95ba9308f48d23a) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve prior turns' thinking by default on the Anthropic provider (Claude and Kimi's Anthropic-compatible mode), matching the Kimi default. Disable with `[thinking] keep = "off"` or `KIMI_MODEL_THINKING_KEEP=off`. + +- [#1451](https://github.com/MoonshotAI/kimi-code/pull/1451) [`16dc940`](https://github.com/MoonshotAI/kimi-code/commit/16dc940834d9cc693b1f0022c4c70ef0004a6102) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Recover chat streaming after a stale background-tab WebSocket instead of requiring a page refresh. + +- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal completion and blocked updates to produce one final user-facing outcome summary from the tool result. + +- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal startup and queue handling so failed starts restore permission mode and queued goals wait behind new user messages. + +- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal token budgets to count model completion tokens and stop without extra continuation steps when the budget is exhausted. + +- [#1452](https://github.com/MoonshotAI/kimi-code/pull/1452) [`244ec07`](https://github.com/MoonshotAI/kimi-code/commit/244ec077f98c2b498cee1d0002978b6963ccfd4d) Thanks [@sailist](https://github.com/sailist)! - Fix kimi -p abandoning background subagents that start late or run long, so their results reach the main agent. + +- [#1457](https://github.com/MoonshotAI/kimi-code/pull/1457) [`260a807`](https://github.com/MoonshotAI/kimi-code/commit/260a80793a95d7796950a00bdc89cf99f8b196ad) Thanks [@liruifengv](https://github.com/liruifengv)! - Respect the --skills-dir flag in interactive mode. + +- [#1445](https://github.com/MoonshotAI/kimi-code/pull/1445) [`809a88c`](https://github.com/MoonshotAI/kimi-code/commit/809a88cb34d2d5d02e43f030530bb1cd320b4a6a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix `/btw [<question>]` opening an empty side chat on the new-session screen. + +- [#1445](https://github.com/MoonshotAI/kimi-code/pull/1445) [`809a88c`](https://github.com/MoonshotAI/kimi-code/commit/809a88cb34d2d5d02e43f030530bb1cd320b4a6a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix `/goal <objective>` silently doing nothing on the new-session screen. + +- [#1445](https://github.com/MoonshotAI/kimi-code/pull/1445) [`809a88c`](https://github.com/MoonshotAI/kimi-code/commit/809a88cb34d2d5d02e43f030530bb1cd320b4a6a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix slash skill activations (for example `/pre-changelog`) silently doing nothing on the new-session screen. + +- [#1437](https://github.com/MoonshotAI/kimi-code/pull/1437) [`743f66e`](https://github.com/MoonshotAI/kimi-code/commit/743f66e547279916d5e37454e78b11eb4b54dca3) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop showing tool-produced `<system>` metadata in tool outputs; failed tools now show their own error text. + +- [#1465](https://github.com/MoonshotAI/kimi-code/pull/1465) [`bfdbce5`](https://github.com/MoonshotAI/kimi-code/commit/bfdbce593f1dd667530cbfb5b10b5659b1968e48) Thanks [@liruifengv](https://github.com/liruifengv)! - Honor explicit Anthropic `max_output_size` settings instead of clamping them to built-in ceilings. + +- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Keep goal tools available to the main agent and return clear messages for invalid goal-control calls. + +- [#1463](https://github.com/MoonshotAI/kimi-code/pull/1463) [`03e78ae`](https://github.com/MoonshotAI/kimi-code/commit/03e78ae19063b38119f27b1bc89097a09614c0ce) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix newer Claude minor versions (e.g. Opus 4.8) defaulting to the family-baseline max output tokens; an uncatalogued minor now reuses the nearest earlier known version's limit. + +- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Show long-running goal wall-clock budget reminders in hours. + +- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Tighten goal-mode guidance so agents continue reasonable work across turns instead of ending goals prematurely. + +- [#1450](https://github.com/MoonshotAI/kimi-code/pull/1450) [`7a65e0d`](https://github.com/MoonshotAI/kimi-code/commit/7a65e0d1c0da515dbd69f1266ba7e75713e0108e) Thanks [@liruifengv](https://github.com/liruifengv)! - Clarify the permission mode descriptions shown by `/permission`, `/auto`, and `/yolo`, and reorder `/auto` and `/yolo` in the command list. + +- [#1448](https://github.com/MoonshotAI/kimi-code/pull/1448) [`65d3017`](https://github.com/MoonshotAI/kimi-code/commit/65d30177adc11a56bdbbe9fbc3c4b92f96efd6bb) Thanks [@RealKai42](https://github.com/RealKai42)! - Record a per-request trace in the session wire log, so model requests can be reconstructed for debugging. Not a user-facing feature. + +## 0.23.0 + +### Minor Changes + +- [#1417](https://github.com/MoonshotAI/kimi-code/pull/1417) [`79b360c`](https://github.com/MoonshotAI/kimi-code/commit/79b360c96ad5d0af6a8c1d3a8df73adc65254d7c) Thanks [@RealKai42](https://github.com/RealKai42)! - Enable Preserved Thinking by default for Kimi models when Thinking is on, keeping prior reasoning across turns. Set `[thinking] keep = "off"` in config.toml (or `KIMI_MODEL_THINKING_KEEP=off`) to disable it. + +- [#1073](https://github.com/MoonshotAI/kimi-code/pull/1073) [`6c0ce09`](https://github.com/MoonshotAI/kimi-code/commit/6c0ce09414bbfd42d8991c88c89b82c088ff2099) Thanks [@sailist](https://github.com/sailist)! - Add server APIs to restore archived sessions and list only archived sessions. + +- [#1369](https://github.com/MoonshotAI/kimi-code/pull/1369) [`f0896a5`](https://github.com/MoonshotAI/kimi-code/commit/f0896a53b01f7e5b9bf5b8f93d2cd7387d765f07) Thanks [@starquakee](https://github.com/starquakee)! - Add experimental progressive tool disclosure (`select_tools`). When the `tool-select` experimental flag is on and the active model declares the `select_tools` capability, MCP tool schemas stay out of the request's top-level `tools[]` (preserving the provider prompt cache); the model loads tools on demand by exact name via the new built-in `select_tools` tool, guided by `<tools_added>/<tools_removed>` announcements. Off by default and inert on models without the capability — behavior is unchanged until a supporting model is catalogued. The SDK additionally maps the `select_tools` capability when building model aliases from a catalog and reports the new flag through `getExperimentalFeatures()`. + +- [#1073](https://github.com/MoonshotAI/kimi-code/pull/1073) [`6c0ce09`](https://github.com/MoonshotAI/kimi-code/commit/6c0ce09414bbfd42d8991c88c89b82c088ff2099) Thanks [@sailist](https://github.com/sailist)! - web: Add an Archived sessions page in Settings to browse and restore archived sessions. Open Settings → Archived to find it. + +- [#1425](https://github.com/MoonshotAI/kimi-code/pull/1425) [`c5e3e80`](https://github.com/MoonshotAI/kimi-code/commit/c5e3e80041a763143934c271d2524ac555d48d2a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Replace the swarm footer with a single inline tool card that shows live subagent progress and the aggregated result, and keep swarm badges stable after refresh. + +### Patch Changes + +- [#1414](https://github.com/MoonshotAI/kimi-code/pull/1414) [`c12f309`](https://github.com/MoonshotAI/kimi-code/commit/c12f30951f2c278bebb42d0f61d67946c42b9577) Thanks [@RealKai42](https://github.com/RealKai42)! - AskUserQuestion now feeds answers back to the model as question text and option labels (e.g. `{"answers":{"Which database?":"Postgres"}}`) instead of synthesized ids like `q_0`/`opt_0_1`, so the model no longer has to map positional ids back to the original options — the wire protocol is unchanged, clients still answer with option ids. Question texts must now be unique per call and option labels unique per question; the web transcript card resolves both the new label form and legacy id transcripts. + +- [#1408](https://github.com/MoonshotAI/kimi-code/pull/1408) [`fc259ab`](https://github.com/MoonshotAI/kimi-code/commit/fc259abdb415fe9ac10132a142bdb5ce507ccda2) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix `@` file completion missing deeply nested files in large projects after adding extra workspace directories. + +- [#1346](https://github.com/MoonshotAI/kimi-code/pull/1346) [`b9258ee`](https://github.com/MoonshotAI/kimi-code/commit/b9258ee07d32ff63afe9a2eb40fce6d136548fb2) Thanks [@liruifengv](https://github.com/liruifengv)! - Show compaction summaries in the TUI after compaction. Press Ctrl-O to show or hide the summary. + +- [#1419](https://github.com/MoonshotAI/kimi-code/pull/1419) [`5ea3ec4`](https://github.com/MoonshotAI/kimi-code/commit/5ea3ec489e0a7d66b844c39ee65162fd6a8ed8b1) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the Bash tool card collapsing in height when a multi-line command finishes with short output, and visually separate the command from its output. + +- [#1410](https://github.com/MoonshotAI/kimi-code/pull/1410) [`1c817df`](https://github.com/MoonshotAI/kimi-code/commit/1c817df1e522f438d4392568b64fc039dc867031) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the edit approval preview shown by ctrl+e to include surrounding context lines, matching the summary panel. + +- [#1421](https://github.com/MoonshotAI/kimi-code/pull/1421) [`1de0286`](https://github.com/MoonshotAI/kimi-code/commit/1de028612c80c38cd6fbe4483c123ded57a0a678) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the Edit tool card jumping in height and flickering while its result streams in. + +- [#1389](https://github.com/MoonshotAI/kimi-code/pull/1389) [`ebdffc7`](https://github.com/MoonshotAI/kimi-code/commit/ebdffc7df7b89dcafcf62f5705eafb50bfbaf5ab) Thanks [@sailist](https://github.com/sailist)! - Fix tool calling with Google Gemini models, including Gemini 3 thinking-signature round-trips across turns. + +- [#1393](https://github.com/MoonshotAI/kimi-code/pull/1393) [`4c43935`](https://github.com/MoonshotAI/kimi-code/commit/4c43935e31170140699a54cba631792872628655) Thanks [@justjavac](https://github.com/justjavac)! - web: Show the correct session search shortcut on Windows. + +- [#1406](https://github.com/MoonshotAI/kimi-code/pull/1406) [`ce41f4b`](https://github.com/MoonshotAI/kimi-code/commit/ce41f4b58d128ae47b0312eab24a845bbc0d08a3) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the collapsed sidebar not hiding and squeezing the conversation layout. + +- [#1413](https://github.com/MoonshotAI/kimi-code/pull/1413) [`913d042`](https://github.com/MoonshotAI/kimi-code/commit/913d042208b4bfe45dc13144c4797dba1cac5d05) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the input box shifting upward after the slash command menu closes. + +- [#1433](https://github.com/MoonshotAI/kimi-code/pull/1433) [`ac5b5e4`](https://github.com/MoonshotAI/kimi-code/commit/ac5b5e4cbfdb050817c9fce7e08dd3bdd8ea354e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix tool components jumping the conversation when expanded or collapsed. + +- [#1394](https://github.com/MoonshotAI/kimi-code/pull/1394) [`e95fc83`](https://github.com/MoonshotAI/kimi-code/commit/e95fc83cc295dccf3e4c748fba087530dba614b6) Thanks [@justjavac](https://github.com/justjavac)! - web: Fix the font size setting so chat text, composer text, and sidebar text follow the selected size. + +- [#1411](https://github.com/MoonshotAI/kimi-code/pull/1411) [`e6e6dd5`](https://github.com/MoonshotAI/kimi-code/commit/e6e6dd53ce9106f47684534a91acb1a803d1ab07) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Stop the chat history from replaying its entrance animation every time a session is opened. + +- [#1409](https://github.com/MoonshotAI/kimi-code/pull/1409) [`578f7d3`](https://github.com/MoonshotAI/kimi-code/commit/578f7d334c7919e5987229c157060b1daae30139) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the end of a reply staying missing after reopening a session. + +- [#1390](https://github.com/MoonshotAI/kimi-code/pull/1390) [`083d0ca`](https://github.com/MoonshotAI/kimi-code/commit/083d0caf0524ed9cc7978007cd0f342f6bd2917e) Thanks [@sailist](https://github.com/sailist)! - Fix sessions that exist on disk but were missing from the session list or returned 404 on direct access, by rebuilding the session index at server startup and keeping it consistent. + +- [#1357](https://github.com/MoonshotAI/kimi-code/pull/1357) [`be7c991`](https://github.com/MoonshotAI/kimi-code/commit/be7c9916b019b19e057301c39bc7944fcac09414) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix queued media messages not loading back into the composer and keep attachments when undoing a message. + +- [#1428](https://github.com/MoonshotAI/kimi-code/pull/1428) [`903e8ed`](https://github.com/MoonshotAI/kimi-code/commit/903e8ed93afc5b35d0fa1d33c86da2b3fae9ba9f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add an Archived sessions entry to the mobile settings sheet and clarify the archive confirmation to mention restoring from Settings. + +- [#1391](https://github.com/MoonshotAI/kimi-code/pull/1391) [`c5c6282`](https://github.com/MoonshotAI/kimi-code/commit/c5c6282f447dba202c79cf0e3b7524712d2c2748) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Render AskUserQuestion answers as a readable option list with the chosen option(s) highlighted, instead of raw JSON. + +- [#1423](https://github.com/MoonshotAI/kimi-code/pull/1423) [`fa6d198`](https://github.com/MoonshotAI/kimi-code/commit/fa6d198b0174ad76aa4ca3c0ea2ed45e099e521b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix an almost-invisible composer input caret and a washed-out strikethrough on completed todos. + +- [#1436](https://github.com/MoonshotAI/kimi-code/pull/1436) [`a5fbcb7`](https://github.com/MoonshotAI/kimi-code/commit/a5fbcb75b4b3ab937536a7a2f621c0374812c753) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Keep the composer toolbar from clipping its controls on narrow windows and phones, with the context ring staying visible at every width. + +- [#1426](https://github.com/MoonshotAI/kimi-code/pull/1426) [`2374bc4`](https://github.com/MoonshotAI/kimi-code/commit/2374bc41c35adc1d2e2b5116559946c8de1b98a8) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Show scheduled-reminder (cron) fires as notice cards in the chat instead of hiding them. + +- [#1391](https://github.com/MoonshotAI/kimi-code/pull/1391) [`c5c6282`](https://github.com/MoonshotAI/kimi-code/commit/c5c6282f447dba202c79cf0e3b7524712d2c2748) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Align the markdown diff code block with the design system: code text keeps the normal ink colour while the sign and a soft row background carry the change, matching the ~/diff panel. + +- [#1434](https://github.com/MoonshotAI/kimi-code/pull/1434) [`4aacddc`](https://github.com/MoonshotAI/kimi-code/commit/4aacddc43222d0a44f202360462617788ca75660) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Show the Kimi icon and clearer titles in web desktop notifications. + +- [#1392](https://github.com/MoonshotAI/kimi-code/pull/1392) [`4963c90`](https://github.com/MoonshotAI/kimi-code/commit/4963c9016fa19d1e01f8dc938c8d250afec87965) Thanks [@sailist](https://github.com/sailist)! - web: Show available skills in the composer before a session is created. + +- [#1438](https://github.com/MoonshotAI/kimi-code/pull/1438) [`d86fa38`](https://github.com/MoonshotAI/kimi-code/commit/d86fa38e119c5834fff13a67194efe8d62c117e1) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Prevent chat text from hyphenating at line breaks and render code without font ligatures. + +- [#1391](https://github.com/MoonshotAI/kimi-code/pull/1391) [`c5c6282`](https://github.com/MoonshotAI/kimi-code/commit/c5c6282f447dba202c79cf0e3b7524712d2c2748) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Drop the stray left indent in the tool-call card body so expanded content aligns with the header. + +## 0.22.3 + +### Patch Changes + +- [#1367](https://github.com/MoonshotAI/kimi-code/pull/1367) [`23daf0f`](https://github.com/MoonshotAI/kimi-code/commit/23daf0f3c199b4aaa9bd9388a2903d7827f98d32) - Revert the recent TUI transcript rendering changes to the original upstream behavior and fix related rendering issues. + +- [#1343](https://github.com/MoonshotAI/kimi-code/pull/1343) [`ec758c7`](https://github.com/MoonshotAI/kimi-code/commit/ec758c747a95555847b8a0275ed0809010c7d5e7) - Add click-to-enlarge for images uploaded in the web chat. Click an image in a message to open it. + +- [#1343](https://github.com/MoonshotAI/kimi-code/pull/1343) [`ec758c7`](https://github.com/MoonshotAI/kimi-code/commit/ec758c747a95555847b8a0275ed0809010c7d5e7) - Fix uploaded videos failing to play in the web chat. + +- [#1371](https://github.com/MoonshotAI/kimi-code/pull/1371) [`5394fea`](https://github.com/MoonshotAI/kimi-code/commit/5394feaabb5d373fab046b3986b10a1180b4991d) - Wait for background subagents to finish and respond to their results before exiting in `kimi -p`, instead of ending the turn early. + +- [#1373](https://github.com/MoonshotAI/kimi-code/pull/1373) [`e715b16`](https://github.com/MoonshotAI/kimi-code/commit/e715b1648c57bd0863edf859cb67db0327b7bb94) - Add `--dangerous-bypass-auth` and `--keep-alive` flags to `kimi server run`, so the server can run without a token on trusted networks and stay alive past the idle timeout. + +- [#1344](https://github.com/MoonshotAI/kimi-code/pull/1344) [`26b9022`](https://github.com/MoonshotAI/kimi-code/commit/26b90225d21bd18f4f7e3b775f3f7f49034afad9) - Add a segmented thinking-level control in the web model picker for models that support multiple reasoning efforts. Open the composer model menu to choose a level. + +## 0.22.2 + +### Patch Changes + +- [#1353](https://github.com/MoonshotAI/kimi-code/pull/1353) [`68ad686`](https://github.com/MoonshotAI/kimi-code/commit/68ad686211760eb1c3e6b5c23eb28ace9009c17f) - Fix duplicated transcript content appearing in scrollback during streaming. + +- [#1340](https://github.com/MoonshotAI/kimi-code/pull/1340) [`e2fe62a`](https://github.com/MoonshotAI/kimi-code/commit/e2fe62a5eff124b816a0f656355fc5bea85e3893) - Fix sessions silently dropping later user messages after a turn was interrupted between a tool call and its result. + +- [#1342](https://github.com/MoonshotAI/kimi-code/pull/1342) [`84d8d5b`](https://github.com/MoonshotAI/kimi-code/commit/84d8d5b06399d29a9d8caba701835061c30a4817) - Have context-compaction notes capture a forward plan for the remaining work — upcoming steps, settled decisions, and foreseeable obstacles — instead of only the immediate next step, so the agent continues more coherently after auto-compaction. + +- [#1340](https://github.com/MoonshotAI/kimi-code/pull/1340) [`e2fe62a`](https://github.com/MoonshotAI/kimi-code/commit/e2fe62a5eff124b816a0f656355fc5bea85e3893) - Fix requests being rejected by strict providers when the model emits duplicate tool call ids. + +- [#1339](https://github.com/MoonshotAI/kimi-code/pull/1339) [`021786f`](https://github.com/MoonshotAI/kimi-code/commit/021786f5a201df5466a963d4d1ac915b3977582b) - Enrich PATH from the user's login shell at startup, so shell commands find user-installed tools (e.g. Homebrew's `gh`) even when kimi-code was launched without the full profile PATH. + +- [#1336](https://github.com/MoonshotAI/kimi-code/pull/1336) [`4c1d0a1`](https://github.com/MoonshotAI/kimi-code/commit/4c1d0a1633c98ae5703addbf86ffe50b81545c08) - Keep automatic background updates from flashing a console window on Windows. + +- [#1332](https://github.com/MoonshotAI/kimi-code/pull/1332) [`93f16c3`](https://github.com/MoonshotAI/kimi-code/commit/93f16c32d71d974f30c3ea3b1134691936ac5f53) - Fix `kimi upgrade` failing on Windows with a spawn error when installing the new version. + +- [#1348](https://github.com/MoonshotAI/kimi-code/pull/1348) [`175b95f`](https://github.com/MoonshotAI/kimi-code/commit/175b95f3af684f7c5447967b9fe7c8a58b6ffe1b) - Fix compressed-image prompts leaking an internal `<system>` compression note into the visible message and the session title. + +- [#1338](https://github.com/MoonshotAI/kimi-code/pull/1338) [`276407d`](https://github.com/MoonshotAI/kimi-code/commit/276407d2a46b03ce32cce02b73c5d485b1b02b17) - Promote the language-matching rule to a dedicated section in the system prompt, so replies and reasoning consistently follow the user's language through long English tool output, while repository artifacts keep project conventions. + +- [#1347](https://github.com/MoonshotAI/kimi-code/pull/1347) [`02da587`](https://github.com/MoonshotAI/kimi-code/commit/02da5877953ce082826ba5ab1a1abd914d82b24a) - In `kimi -p` runs, wait for background subagents to finish before exiting when `background.keep_alive_on_exit` is enabled. Set `keep_alive_on_exit = true` to let concurrent background subagents complete. + +- [#1349](https://github.com/MoonshotAI/kimi-code/pull/1349) [`e9db9ca`](https://github.com/MoonshotAI/kimi-code/commit/e9db9cafcf7a0d26122b2cac247d866d7724fd7a) - Record model response ids in session wire logs to make individual model requests easier to trace. + +- [#1345](https://github.com/MoonshotAI/kimi-code/pull/1345) [`3ed22e3`](https://github.com/MoonshotAI/kimi-code/commit/3ed22e35a4ee09ce353e699406c6c994423ff39f) - Keep subagent cards at a stable height and show a live status spinner with a compact two-row activity window. + +- [#1305](https://github.com/MoonshotAI/kimi-code/pull/1305) [`9091627`](https://github.com/MoonshotAI/kimi-code/commit/909162725770700efd3051f4cfa68156d9b84fa8) - Add a TUI preference to keep rapid multi-line pastes from submitting line by line when bracketed paste is unavailable. Set `disable_paste_burst = true` in `tui.toml` to turn it off. + +- [#1328](https://github.com/MoonshotAI/kimi-code/pull/1328) [`01b65bd`](https://github.com/MoonshotAI/kimi-code/commit/01b65bdddc28c7c492096000103687f6a507e353) - Rebuild the web design-system easter egg as an in-app overlay that uses the app's real design tokens, so it stays in sync instead of drifting as a separate copy. + +## 0.22.1 + +### Patch Changes + +- [#1304](https://github.com/MoonshotAI/kimi-code/pull/1304) [`0fc0ae3`](https://github.com/MoonshotAI/kimi-code/commit/0fc0ae380b09aa96aad0eff1ae66f239e061d01a) - When large images are compressed, tell the model the original and delivered image details. Keep the original image available, and support cropped or full-resolution reads for fine details. + +- [#1315](https://github.com/MoonshotAI/kimi-code/pull/1315) [`b40bb71`](https://github.com/MoonshotAI/kimi-code/commit/b40bb7139939eb2ba734ce5dd4871b894d7033e8) - Fix TUI rendering bugs that caused the screen to go blank and the input box to disappear. + +- [#1303](https://github.com/MoonshotAI/kimi-code/pull/1303) [`2639786`](https://github.com/MoonshotAI/kimi-code/commit/2639786ce578f15c020a2c11c344797dae18de61) - Fix the TUI crashing when the terminal is resized to a very narrow width while the input contains CJK or emoji text. + +- [#1315](https://github.com/MoonshotAI/kimi-code/pull/1315) [`b40bb71`](https://github.com/MoonshotAI/kimi-code/commit/b40bb7139939eb2ba734ce5dd4871b894d7033e8) - Clear the screen fully when starting a new session via /new, /clear, or a session switch. + +- [#1301](https://github.com/MoonshotAI/kimi-code/pull/1301) [`c3653a1`](https://github.com/MoonshotAI/kimi-code/commit/c3653a1c50ffa3856484599e132980628eb9fca4) - Show an up arrow on the web composer send button. + +- [#1290](https://github.com/MoonshotAI/kimi-code/pull/1290) [`3ea84a5`](https://github.com/MoonshotAI/kimi-code/commit/3ea84a56e4dfdeaddd58add5b269be0342f3f986) - Fix the session search dialog showing a horizontal scrollbar for long session titles or snippets. + +- [#1316](https://github.com/MoonshotAI/kimi-code/pull/1316) [`5322c63`](https://github.com/MoonshotAI/kimi-code/commit/5322c638895a934c1ce220fefed54f5077d2a49e) - Fix web tooltips that could get stuck on screen when their trigger element is removed while open. + +- [#1319](https://github.com/MoonshotAI/kimi-code/pull/1319) [`e8ab7ca`](https://github.com/MoonshotAI/kimi-code/commit/e8ab7ca78661de7f00a8196444be1db93e7c14b4) - Fix the sidebar session row shifting its title and status badges when hovered. + +- [#1293](https://github.com/MoonshotAI/kimi-code/pull/1293) [`6a469b3`](https://github.com/MoonshotAI/kimi-code/commit/6a469b3e07022e56b29b1fd8a7c58df36b2111fe) - Refresh the web UI icon set and unify the message copy and undo button hover states and tooltips. + +- [#1311](https://github.com/MoonshotAI/kimi-code/pull/1311) [`b40649b`](https://github.com/MoonshotAI/kimi-code/commit/b40649b2ae7a4b6a0aea04e32eba200555393064) - Remove duplicate newline-shortcut handling from the prompt editor. + +- [#1317](https://github.com/MoonshotAI/kimi-code/pull/1317) [`78a058a`](https://github.com/MoonshotAI/kimi-code/commit/78a058acd2fc91de5cca0c1d66d415ee35884889) - Remove the experimental micro compaction feature and its toggle from the experiments panel. + +- [#1283](https://github.com/MoonshotAI/kimi-code/pull/1283) [`ea55911`](https://github.com/MoonshotAI/kimi-code/commit/ea55911062eefcb0414cfddb84c8a4494c45f363) - Improve compaction handoff summaries for more reliable resumed sessions. They now keep the latest intent, key tool results, decisions, open questions, and context to re-check. + +- [#1295](https://github.com/MoonshotAI/kimi-code/pull/1295) [`77eb3a9`](https://github.com/MoonshotAI/kimi-code/commit/77eb3a9fe40c93fa32e335f07160b8128355bab6) - Save shell commands to input history and recall them in bash mode. Press Up on an empty `!` prompt to browse previous shell commands. + +- [#1316](https://github.com/MoonshotAI/kimi-code/pull/1316) [`5322c63`](https://github.com/MoonshotAI/kimi-code/commit/5322c638895a934c1ce220fefed54f5077d2a49e) - Trim redundant and incorrect tooltips in the web UI. + +- [#1320](https://github.com/MoonshotAI/kimi-code/pull/1320) [`444e6b1`](https://github.com/MoonshotAI/kimi-code/commit/444e6b15f0e53b6c4d75d1bfdc0b35639dce6f4c) - Fix the web UI becoming sluggish after opening many sessions. + +- [#1322](https://github.com/MoonshotAI/kimi-code/pull/1322) [`5441ad1`](https://github.com/MoonshotAI/kimi-code/commit/5441ad1838a5cfa1f3df0ca2ee1524e1433fb513) - Let the web sidebar collapse an expanded workspace session list back to its first page. + +## 0.22.0 + +### Minor Changes + +- [#1243](https://github.com/MoonshotAI/kimi-code/pull/1243) [`ace7901`](https://github.com/MoonshotAI/kimi-code/commit/ace79010669d19ad175bc25443b6efb41ca2e2ac) - Automatically compress oversized images before they reach the model. Whatever the source — pasted into the CLI, uploaded from the web/desktop client, sent over ACP, read via `ReadMediaFile`, or returned by an MCP tool — images are downsampled (longest edge ≤ 2000px) and re-encoded to fit a per-image byte budget, cutting vision-token cost and avoiding provider image-size errors. Screenshots stay lossless PNG and only degrade to JPEG when the byte budget cannot otherwise be met. Compression runs as an input-stage step at each ingestion point (while the content part is built), and guards against decompression bombs by skipping absurdly large pixel/byte payloads before decoding. Best-effort: if it fails for any reason the original image is sent unchanged. + +- [#1262](https://github.com/MoonshotAI/kimi-code/pull/1262) [`c070fbe`](https://github.com/MoonshotAI/kimi-code/commit/c070fbeddeb1c147d8859a76046f9465f696c9cb) - Add model alias overrides so manual thinking effort levels and model metadata survive provider catalog refreshes. Set them under `[models."<alias>".overrides]`. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Refresh the web UI with a new design system, including updated colors, typography, spacing, light and dark palettes, restyled tooltips, and subtle enter/exit and expand/collapse animations. + +### Patch Changes + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show draft pull requests with a distinct draft status instead of displaying them as open. + +- [#1254](https://github.com/MoonshotAI/kimi-code/pull/1254) [`7859b0a`](https://github.com/MoonshotAI/kimi-code/commit/7859b0afe8898852806e5a0c21b9dd52cb82f834) - Fix the transcript jumping to the top when scrolling up through history during streaming output. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix plan, swarm, and goal modes being shared across sessions in the web UI; each session now keeps its own toggles. + +- [#1264](https://github.com/MoonshotAI/kimi-code/pull/1264) [`003733c`](https://github.com/MoonshotAI/kimi-code/commit/003733c751584ce30d8ebae4f5e608f0df049d32) - Hide the unsupported Off option in the /model thinking switcher for always-on models that already expose multiple effort levels. + +- [#1272](https://github.com/MoonshotAI/kimi-code/pull/1272) [`54703d9`](https://github.com/MoonshotAI/kimi-code/commit/54703d9457dcda7bc782301fc2dbb41a2c8d7293) - Release pasted images and streaming timers once they are no longer shown, so memory stops growing in long sessions. + +- [#1272](https://github.com/MoonshotAI/kimi-code/pull/1272) [`54703d9`](https://github.com/MoonshotAI/kimi-code/commit/54703d9457dcda7bc782301fc2dbb41a2c8d7293) - Fix the terminal being left in raw mode with a hidden cursor and disabled flow control after a crash or abrupt exit. + +- [#1265](https://github.com/MoonshotAI/kimi-code/pull/1265) [`8cfb165`](https://github.com/MoonshotAI/kimi-code/commit/8cfb1657ad7bf525269df4ab6cf5c12aa1d406a9) - Reduce the default TUI transcript window to keep long sessions responsive. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Replace the Explore and Native theme options with a single chat layout and a Blue or Black accent-color setting. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show time, duration, connection, and stack details in web error and warning toasts. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix an active workspace showing only its five most recent sessions on load, so it now keeps loading older sessions from the last 12 hours. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Reduce the web composer's default height for a more compact empty state, and fix ArrowUp recalling the previous message while editing a multi-line draft; ArrowUp now recalls only from the very start of the text and is disabled in the expanded editor. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix the Thinking-by-default setting not taking effect, so new sessions correctly start with thinking enabled. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix spurious errors from the web question, approval, and task actions when the action was already complete, and add loading feedback so each click is acknowledged immediately. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show queued prompts inline below the running turn in the web chat, and split Stop into its own button so Send no longer interrupts. + +- [#1278](https://github.com/MoonshotAI/kimi-code/pull/1278) [`bbda90a`](https://github.com/MoonshotAI/kimi-code/commit/bbda90af846ca66232158d2e9605d3d59a7e3a49) - Hide the conversation outline when there is not enough room to expand its labels, so it no longer clips against the window edge. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show the conversation outline as one entry per user query that expands into a labeled list on hover. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Remove the fade-out animation when undoing a message in the web chat. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Improve session search with a Cmd/Ctrl+K palette that filters by title, workspace, and last prompt with highlighted matches. Press Cmd+K or Ctrl+K to open it. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Group consecutive tool calls into a collapsible stack with per-tool renderers, including diff line-count chips for edits and inline previews for image, video, and audio results. + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Use one consistent modal dialog for confirmations in the web UI (archive session, delete workspace, delete provider, undo message, and mode toggles). + +- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Add workspace sorting by manual order or last-edited time, plus collapse-all and expand-all controls, to the sidebar. + +## 0.21.1 + +### Patch Changes + +- [#1256](https://github.com/MoonshotAI/kimi-code/pull/1256) [`0cc02ac`](https://github.com/MoonshotAI/kimi-code/commit/0cc02ac67d465d1d4d7fe070422bab17053cdaa3) - Keep the waiting spinner visible while encrypted reasoning streams, fixing a blank spinner-less gap before the first response text appears. + +## 0.21.0 + +### Minor Changes + +- [#1204](https://github.com/MoonshotAI/kimi-code/pull/1204) [`5cb80ce`](https://github.com/MoonshotAI/kimi-code/commit/5cb80ce879406d239048c32d61202778cb860e58) - Plugins can now provide slash commands via a `commands` field in their manifest, registered as `<plugin>:<command>` and invoked with `$ARGUMENTS` expansion. + +- [#1214](https://github.com/MoonshotAI/kimi-code/pull/1214) [`86e0c92`](https://github.com/MoonshotAI/kimi-code/commit/86e0c9201ed58c7c1ce5543b1dfb47a4cf5117f6) - Rework conversation compaction: + + - Keep only recent user prompts plus a single user-role summary; drop assistant and tool messages. + - Repair tool_use/tool_result adjacency before sending, fixing a strict-provider HTTP 400 when a tool call and its result became non-adjacent. + - Merge consecutive user turns for strict providers (Gemini/Vertex), fixing an HTTP 400 ("roles must alternate") after compaction or when a turn is steered in right after a tool result. + - Micro-compaction now defaults off. + +- [#1132](https://github.com/MoonshotAI/kimi-code/pull/1132) [`108299b`](https://github.com/MoonshotAI/kimi-code/commit/108299be3cdffc31a23f64efd3ff5ba50976b412) - Refactor the thinking effort system + +### Patch Changes + +- [#1231](https://github.com/MoonshotAI/kimi-code/pull/1231) [`ceb27f5`](https://github.com/MoonshotAI/kimi-code/commit/ceb27f5e449e177493f320d90e292487a8fc3410) - Add a server-side key-value store API for persisting web UI preferences to the user's data directory. + +- [#1220](https://github.com/MoonshotAI/kimi-code/pull/1220) [`ec51324`](https://github.com/MoonshotAI/kimi-code/commit/ec51324230484f2ebaad1ab0aebf2e38f531d914) - Add a double-Esc shortcut to open the undo selector. Press Esc twice while idle to undo. + +- [#1223](https://github.com/MoonshotAI/kimi-code/pull/1223) [`80e6888`](https://github.com/MoonshotAI/kimi-code/commit/80e6888e34e4362247c0eac5b77340df014ba286) - Fix @ file mentions not opening when typed inside a slash command argument. + +- [#1233](https://github.com/MoonshotAI/kimi-code/pull/1233) [`020992c`](https://github.com/MoonshotAI/kimi-code/commit/020992c286f0f6bff6a038a7c7bd7e9db639e3c9) - Force-exit headless runs (`kimi -p`) so a stray ref'd handle left over from the run can't keep a completed run alive until an external timeout, and bound prompt cleanup so a wedged shutdown step can't hang shutdown. + +- [#1225](https://github.com/MoonshotAI/kimi-code/pull/1225) [`659062d`](https://github.com/MoonshotAI/kimi-code/commit/659062d11cc272fe631fc6d4faf64d0e0b1a0142) - Show file path completions when typing `/` in shell mode (`!`). + +- [#1236](https://github.com/MoonshotAI/kimi-code/pull/1236) [`bfe8e6a`](https://github.com/MoonshotAI/kimi-code/commit/bfe8e6ace3cda76b1991bf29c25b9444611d5512) - Fix adding a workspace by path in the web UI failing silently when the daemon rejects the path; it now shows an error instead of a broken workspace. + +- [#1221](https://github.com/MoonshotAI/kimi-code/pull/1221) [`a3f9cec`](https://github.com/MoonshotAI/kimi-code/commit/a3f9cec8a975f11e37e992e42f954789ed394207) - Fix duplicate workspaces showing in the web sidebar when the same folder is registered more than once. + +- [#1241](https://github.com/MoonshotAI/kimi-code/pull/1241) [`8ac337a`](https://github.com/MoonshotAI/kimi-code/commit/8ac337a2b2ac800aa79a373459308abb6c9e63bb) - Stop a malformed message history from permanently bricking a session on strict providers (Anthropic). The request is repaired before sending — orphaned tool calls are closed and empty/whitespace-only text blocks dropped — and if the provider still rejects its structure, it is resent once with a wire-compliant rebuild. + +- [#1228](https://github.com/MoonshotAI/kimi-code/pull/1228) [`42e37eb`](https://github.com/MoonshotAI/kimi-code/commit/42e37eb898b722829d2ec83e909525ff18e336a5) - Split LLM streaming timing in the session log and `KIMI_CODE_DEBUG=1` output into client vs. API-server portions, so slow turns can be attributed without parsing the wire log. Time-to-first-token splits into the API-server portion (network + server) and the client portion (in-process request building); the decode window splits into time awaiting tokens from the server and time the client spends processing each streamed chunk. + +- [#1234](https://github.com/MoonshotAI/kimi-code/pull/1234) [`882cf35`](https://github.com/MoonshotAI/kimi-code/commit/882cf355a9cb45bb5b3424a27b953bde8e106bb0) - Hide the provider management dialog in the web UI until the server supports it. + +- [#1226](https://github.com/MoonshotAI/kimi-code/pull/1226) [`7f05f58`](https://github.com/MoonshotAI/kimi-code/commit/7f05f589e7bc77a2f26463a41317ff7087e3c3a0) - Add Mermaid diagram rendering to the web chat. Fenced `mermaid` blocks in assistant responses now render as diagrams. KaTeX math and Mermaid diagram parsing also run in Web Workers to keep the UI responsive during live streaming. + +- [#1232](https://github.com/MoonshotAI/kimi-code/pull/1232) [`aa6b0d0`](https://github.com/MoonshotAI/kimi-code/commit/aa6b0d065ee888056c3812781483ddb74739897f) - Always show the usage-data opt-out toggle in the web settings with a clearer label and description. + +- [#1234](https://github.com/MoonshotAI/kimi-code/pull/1234) [`882cf35`](https://github.com/MoonshotAI/kimi-code/commit/882cf355a9cb45bb5b3424a27b953bde8e106bb0) - Fix the web workspace rename not persisting after a page refresh. + +## 0.20.3 + +### Patch Changes + +- [#1207](https://github.com/MoonshotAI/kimi-code/pull/1207) [`14d9e98`](https://github.com/MoonshotAI/kimi-code/commit/14d9e98903f30f83199e30b5fa20b3c61ab28781) - Refresh provider model lists automatically in the background instead of only at startup, so newly available models appear without restarting. + +- [#1191](https://github.com/MoonshotAI/kimi-code/pull/1191) [`0df1812`](https://github.com/MoonshotAI/kimi-code/commit/0df18125022103dabb149b4f26f90959b669187b) - Fix provider error messages rendering as blank lines in the TUI when the server returns an HTML error page. + +- [#1212](https://github.com/MoonshotAI/kimi-code/pull/1212) [`636ccc4`](https://github.com/MoonshotAI/kimi-code/commit/636ccc40f19f259bdd6653b2ca563a75b3548e23) - Fix the web composer being hidden behind the mobile Safari toolbar and the page auto-zooming when the composer is focused. + +- [#1068](https://github.com/MoonshotAI/kimi-code/pull/1068) [`c82dcf9`](https://github.com/MoonshotAI/kimi-code/commit/c82dcf9cd8276eddf6acbf1030d1712b83a38083) - Glob now uses ripgrep, so it respects .gitignore by default, supports brace patterns, returns only files, and keeps partial results with a warning when some directories are unreadable. + +- [#1209](https://github.com/MoonshotAI/kimi-code/pull/1209) [`0635387`](https://github.com/MoonshotAI/kimi-code/commit/063538744f64a1bd3da6f37ebd0643d10bfc068f) - Align malformed tool call argument handling with schema validation fallback. + +## 0.20.2 + +### Patch Changes + +- [#1166](https://github.com/MoonshotAI/kimi-code/pull/1166) [`dfcfdfd`](https://github.com/MoonshotAI/kimi-code/commit/dfcfdfd9ddbe14fb6e358694394e1ddcc21b8911) - Add an optional exclude_empty parameter to the session list API to omit sessions that have no messages. + +- [#1156](https://github.com/MoonshotAI/kimi-code/pull/1156) [`794db55`](https://github.com/MoonshotAI/kimi-code/commit/794db55538e01b4bf0c008c493de5d8b8bf67c5d) - Cap compaction output at 128k tokens by default to avoid provider max_tokens errors. + +- [#1129](https://github.com/MoonshotAI/kimi-code/pull/1129) [`d02b5c4`](https://github.com/MoonshotAI/kimi-code/commit/d02b5c49844d65e005632fafcb1c172a7d32bfbe) - Fix compaction ignoring the configured max output size. + +- [#1188](https://github.com/MoonshotAI/kimi-code/pull/1188) [`db5fbc5`](https://github.com/MoonshotAI/kimi-code/commit/db5fbc53c00c9945fc1fa98c69c4e5c7efb8077e) - Fix unnecessary full-screen redraws when typing in the input box or toggling the slash panel. + +- [#1187](https://github.com/MoonshotAI/kimi-code/pull/1187) [`97f9263`](https://github.com/MoonshotAI/kimi-code/commit/97f9263c6f13ead5edc051f96993f8d1d7d5ec6f) - Fix debug timing output lingering after undoing a turn. + +- [#1163](https://github.com/MoonshotAI/kimi-code/pull/1163) [`ff6e8bb`](https://github.com/MoonshotAI/kimi-code/commit/ff6e8bbd7c328dcc6575902cfd0cb3e522f20948) - Fix the web composer occasionally keeping typed text after sending the first message of a new session. + +- [#1189](https://github.com/MoonshotAI/kimi-code/pull/1189) [`04b3492`](https://github.com/MoonshotAI/kimi-code/commit/04b3492e740dad5fca2af9f66eca98da3e14058a) - Fix working tips getting squeezed against the agent swarm progress bar. + +- [#1159](https://github.com/MoonshotAI/kimi-code/pull/1159) [`23a553b`](https://github.com/MoonshotAI/kimi-code/commit/23a553bb91e9ee794aaf769f78f5acec739aec85) - In the bundled web UI, `/new` and `/clear` are now aliases that open the session onboarding composer and focus the input. iOS auto-zoom is prevented by keeping text inputs at 16px instead of disabling viewport scaling. + +- [#1186](https://github.com/MoonshotAI/kimi-code/pull/1186) [`821847c`](https://github.com/MoonshotAI/kimi-code/commit/821847cb4b88d9128014609aad307ab8d9e9a5f3) - Add `KIMI_CODE_CUSTOM_HEADERS` for custom outbound LLM request headers and send the `User-Agent` header to non-Kimi providers. Set `KIMI_CODE_CUSTOM_HEADERS` to newline-separated `Name: Value` lines. + +- [#1186](https://github.com/MoonshotAI/kimi-code/pull/1186) [`821847c`](https://github.com/MoonshotAI/kimi-code/commit/821847cb4b88d9128014609aad307ab8d9e9a5f3) - Route managed Kimi Code models on the Anthropic-compatible protocol through the beta Messages API. + +- [#1170](https://github.com/MoonshotAI/kimi-code/pull/1170) [`cf558cd`](https://github.com/MoonshotAI/kimi-code/commit/cf558cd74267393d6497ddedf25e192eaac4f94b) - Recover from provider 413 context overflows by compacting before retrying. + +- [#1170](https://github.com/MoonshotAI/kimi-code/pull/1170) [`cf558cd`](https://github.com/MoonshotAI/kimi-code/commit/cf558cd74267393d6497ddedf25e192eaac4f94b) - Support the Anthropic-compatible protocol for managed Kimi Code, including video input. + +- [#1186](https://github.com/MoonshotAI/kimi-code/pull/1186) [`821847c`](https://github.com/MoonshotAI/kimi-code/commit/821847cb4b88d9128014609aad307ab8d9e9a5f3) - Add provider type and protocol attributes to turn and API error telemetry. + +- [#1155](https://github.com/MoonshotAI/kimi-code/pull/1155) [`54baf5d`](https://github.com/MoonshotAI/kimi-code/commit/54baf5d07fe718b70b8840e509a905ac48b1ccac) - Upgrade web markdown renderer dependencies (katex, markstream-vue, shiki) for bug fixes and performance improvements. + +- [#1162](https://github.com/MoonshotAI/kimi-code/pull/1162) [`b070846`](https://github.com/MoonshotAI/kimi-code/commit/b0708464f4160f7b73f25a520e493bf87e92149f) - Rework the web ask-user-question card into a step-by-step wizard so multi-question navigation and the final Submit action are easier to see. + +- [#1179](https://github.com/MoonshotAI/kimi-code/pull/1179) [`fc3d69d`](https://github.com/MoonshotAI/kimi-code/commit/fc3d69dbdc965e525b5486a6b91e4ec44194ca97) - Add a completion sound and question notifications to the web UI, with separate Settings toggles for completion notifications, question notifications, and sound. Question notifications default off so question text only reaches your desktop after you opt in. + +- [#1165](https://github.com/MoonshotAI/kimi-code/pull/1165) [`f3b1532`](https://github.com/MoonshotAI/kimi-code/commit/f3b15322da518b0e3d0560d19651435793c790d9) - Replace the web composer attach button's plus icon with an image icon. + +- [#1167](https://github.com/MoonshotAI/kimi-code/pull/1167) [`c63edd5`](https://github.com/MoonshotAI/kimi-code/commit/c63edd5bf6d764c3ab771cb697a334ac100a0944) - In the bundled web UI, a new session is now created only when the first message is sent, so + New without a workspace opens the composer instead of making an empty session. + +- [#1166](https://github.com/MoonshotAI/kimi-code/pull/1166) [`dfcfdfd`](https://github.com/MoonshotAI/kimi-code/commit/dfcfdfd9ddbe14fb6e358694394e1ddcc21b8911) - Hide unused "New Session" entries from the web session list by default. + +- [#1181](https://github.com/MoonshotAI/kimi-code/pull/1181) [`1dab2c2`](https://github.com/MoonshotAI/kimi-code/commit/1dab2c2268af6f74464b6573981c1e1bb4bda703) - Restore each session's scroll position when switching back to it in the web UI. + +- [#1181](https://github.com/MoonshotAI/kimi-code/pull/1181) [`1dab2c2`](https://github.com/MoonshotAI/kimi-code/commit/1dab2c2268af6f74464b6573981c1e1bb4bda703) - Keep the open side panel when switching between sessions in the web UI. + +- [#1166](https://github.com/MoonshotAI/kimi-code/pull/1166) [`dfcfdfd`](https://github.com/MoonshotAI/kimi-code/commit/dfcfdfd9ddbe14fb6e358694394e1ddcc21b8911) - Remove the /sessions slash command from the web UI; the sidebar already covers session browsing. + +- [#1181](https://github.com/MoonshotAI/kimi-code/pull/1181) [`1dab2c2`](https://github.com/MoonshotAI/kimi-code/commit/1dab2c2268af6f74464b6573981c1e1bb4bda703) - Keep unsent composer attachments scoped to their session in the web UI, so switching sessions no longer leaks them into another session's next message. + +- [#1161](https://github.com/MoonshotAI/kimi-code/pull/1161) [`d968642`](https://github.com/MoonshotAI/kimi-code/commit/d968642384f672295756394ee07a536dbfdb4dfd) - Show the first five sessions per workspace in the web sidebar instead of ten. + +- [#1181](https://github.com/MoonshotAI/kimi-code/pull/1181) [`1dab2c2`](https://github.com/MoonshotAI/kimi-code/commit/1dab2c2268af6f74464b6573981c1e1bb4bda703) - Scope the web composer's up/down input history to the current session instead of sharing it across all sessions. + +## 0.20.1 + +### Patch Changes + +- [#1125](https://github.com/MoonshotAI/kimi-code/pull/1125) [`e9a3b7c`](https://github.com/MoonshotAI/kimi-code/commit/e9a3b7c83a623c7323da509ba885567c465093fc) - Add an `update` alias for the `kimi upgrade` command. Run `kimi update` to upgrade to the latest version. + +- [#1122](https://github.com/MoonshotAI/kimi-code/pull/1122) [`820d77a`](https://github.com/MoonshotAI/kimi-code/commit/820d77ab4cfad7752358a4692fd3d7def49f005d) - Show the done / in progress / pending breakdown of hidden todos in the collapsed todo panel. + +- [#1131](https://github.com/MoonshotAI/kimi-code/pull/1131) [`76c643b`](https://github.com/MoonshotAI/kimi-code/commit/76c643bcb6da447c8c47728b4f58512a7a11cfa6) - Cap completion tokens to the remaining context window for chat-completions providers, avoiding context-overflow and invalid max_tokens errors. + +- [#1120](https://github.com/MoonshotAI/kimi-code/pull/1120) [`e736349`](https://github.com/MoonshotAI/kimi-code/commit/e736349a7c8ff55b73e05cc0192dfaf0114745fa) - Add optional feedback attachments for diagnostic logs and codebase context. + +- [#1135](https://github.com/MoonshotAI/kimi-code/pull/1135) [`bf51fb7`](https://github.com/MoonshotAI/kimi-code/commit/bf51fb7a105b2f34a59ed4e83d2588e790cfb086) - Fix the local server failing to start on Windows after the first run because the persistent token file's synthesized mode was rejected as too permissive. + +- [#1102](https://github.com/MoonshotAI/kimi-code/pull/1102) [`9c97161`](https://github.com/MoonshotAI/kimi-code/commit/9c9716125e104b217540d0591229d03c6d676ead) - Harden the default system prompt and built-in tool descriptions: stop the agent from blocking on background tasks it should let run, keep its guidance matched to the tools each profile actually provides, and surface tool-result details (fetched-page mode, Grep match totals) it previously missed. + +- [#1127](https://github.com/MoonshotAI/kimi-code/pull/1127) [`184acf5`](https://github.com/MoonshotAI/kimi-code/commit/184acf5db521a964a8af9dfdb1502121a9be76dc) - Plugins can now declare hooks in their manifest to run scripts on lifecycle events. + +- [#1128](https://github.com/MoonshotAI/kimi-code/pull/1128) [`0886bff`](https://github.com/MoonshotAI/kimi-code/commit/0886bff2bcd3aed954990c948201d84787c0f3f3) - Add a --allowed-host flag to kimi server run that lets extra Host header values pass the DNS-rebinding check, and include allow guidance in the 403 error message. Pass --allowed-host <host> to allow an extra host. + +- [#1119](https://github.com/MoonshotAI/kimi-code/pull/1119) [`b0b2aee`](https://github.com/MoonshotAI/kimi-code/commit/b0b2aee8c5a496c2b679fc9dbbc05e3d1934d5d9) - Keep the terminal responsive in long conversations by caching rendered message lines. + +- [#1119](https://github.com/MoonshotAI/kimi-code/pull/1119) [`b0b2aee`](https://github.com/MoonshotAI/kimi-code/commit/b0b2aee8c5a496c2b679fc9dbbc05e3d1934d5d9) - Keep long sessions responsive by retaining only recent turns in the transcript and collapsing older steps within each turn. + +- [#1121](https://github.com/MoonshotAI/kimi-code/pull/1121) [`81ba48f`](https://github.com/MoonshotAI/kimi-code/commit/81ba48f45534e133947c4e5e78907c2ad0db0b90) - Make the web chat input grow with its content and add an expandable editor for longer messages. + +- [#1133](https://github.com/MoonshotAI/kimi-code/pull/1133) [`f1c8175`](https://github.com/MoonshotAI/kimi-code/commit/f1c8175f9c5766f6a928fd07fb680e3159c564b0) - Fix the /web slash command not carrying the server token, so the opened web UI signs in automatically and the token is shown before the terminal exits. + +## 0.20.0 + +### Minor Changes + +- [#1079](https://github.com/MoonshotAI/kimi-code/pull/1079) [`2db5fc2`](https://github.com/MoonshotAI/kimi-code/commit/2db5fc20ecdf3212afd47e7c26195e428f8eddd5) - Add shell mode for running shell commands. + Type `!` in the input box to enable it. + The command output is visible to the AI. + For long-running commands, press Ctrl+B to move them to the background. + For example, you can run `!gh auth login` to sign in to the GitHub CLI without opening a new terminal, so Kimi can use `gh`. + +- [#1088](https://github.com/MoonshotAI/kimi-code/pull/1088) [`0030f76`](https://github.com/MoonshotAI/kimi-code/commit/0030f76c5cc6465c5a6646c166375127d83696d3) - Add a confirmation prompt before installing third-party plugins. + +- [#1066](https://github.com/MoonshotAI/kimi-code/pull/1066) [`3554f7e`](https://github.com/MoonshotAI/kimi-code/commit/3554f7e7d6e472413aa7a9873d7a2eef5f2b819c) - Show update badges on the /plugins Installed tab, where Enter now installs the available update and I opens plugin details. + +- [#1025](https://github.com/MoonshotAI/kimi-code/pull/1025) [`5ef66dd`](https://github.com/MoonshotAI/kimi-code/commit/5ef66ddfeda2f23c40fc0cf53225cdaf3cc1147d) - Redesign `/plugins` as a single tabbed panel: **Installed** (manage installed + plugins — toggle, remove, MCP, details, reload), **Official** (Kimi-maintained + marketplace plugins), **Third-party** (marketplace plugins from other + publishers), and **Custom** (install straight from a GitHub URL, zip URL, or + local path). `Tab` / `Shift-Tab` switch tabs. The Official and Third-party + catalogs load lazily, so `/plugins` opens instantly and keeps working offline — + a marketplace fetch failure is shown inline instead of closing the panel. The + tab strip is shared with the `/model` provider tabs via the new `renderTabStrip` + helper. + +- [#1006](https://github.com/MoonshotAI/kimi-code/pull/1006) [`60dfb68`](https://github.com/MoonshotAI/kimi-code/commit/60dfb68a2d4c342cfbad5f48d4d269fb6cdd43c0) - Add server authentication and safe `--host` exposure. The local server now + requires a per-start bearer token on all API and WebSocket calls (the CLI reads + it automatically), enforces Host/Origin checks, and gains `--host` with a + public-binding hardening tier: mandatory `KIMI_CODE_PASSWORD`, TLS (or + `--insecure-no-tls`), auth-failure rate limiting, disabled remote + shutdown/terminals, and security response headers. See `packages/server/SECURITY.md`. + +- [#1040](https://github.com/MoonshotAI/kimi-code/pull/1040) [`6664038`](https://github.com/MoonshotAI/kimi-code/commit/66640380ebf60141994986beadf5347617f82814) - Replace silent AGENTS.md truncation with a visible warning in the TUI status bar and web UI. + +- [#1101](https://github.com/MoonshotAI/kimi-code/pull/1101) [`3ea6ac2`](https://github.com/MoonshotAI/kimi-code/commit/3ea6ac278d2e57bb859ab423704bbd0fb2033c72) - Show the plan body and approach choices in the plan review card when exiting plan mode in the web UI. + +- [#1103](https://github.com/MoonshotAI/kimi-code/pull/1103) [`18f7c34`](https://github.com/MoonshotAI/kimi-code/commit/18f7c34a0739dab454af1f09d951a1bbf278cccb) - Show a line-by-line diff when the agent edits or writes a file in the web chat. + +### Patch Changes + +- [#1072](https://github.com/MoonshotAI/kimi-code/pull/1072) [`a86bb97`](https://github.com/MoonshotAI/kimi-code/commit/a86bb9757d99f32983e82a6a82fd3ccaab691b1a) - Improve the image paste hint. + +- [#1076](https://github.com/MoonshotAI/kimi-code/pull/1076) [`500677a`](https://github.com/MoonshotAI/kimi-code/commit/500677ab8baf9081b73a35df5fbbcfc49cb2f9b7) - Fix Ctrl-C during compaction so it clears a pending editor draft first instead of cancelling immediately. + +- [#1067](https://github.com/MoonshotAI/kimi-code/pull/1067) [`0e227ba`](https://github.com/MoonshotAI/kimi-code/commit/0e227ba18aec793aa4c233be7c578068ae91e604) - Fix explore subagents silently losing git context when git commands time out or the directory is not a repository. + +- [#1075](https://github.com/MoonshotAI/kimi-code/pull/1075) [`3aaf1e5`](https://github.com/MoonshotAI/kimi-code/commit/3aaf1e58037c4045aaa3b9fbabaffa158c60d2ca) - Fix a startup crash on Linux caused by an unhandled native clipboard error. + +- [#1094](https://github.com/MoonshotAI/kimi-code/pull/1094) [`8ee5c0f`](https://github.com/MoonshotAI/kimi-code/commit/8ee5c0ff813d361733226a1606e7c724e5e38f2e) - Fix the terminal window repeatedly losing focus on Linux Wayland, which broke IME input. + +- [#1057](https://github.com/MoonshotAI/kimi-code/pull/1057) [`ee69e16`](https://github.com/MoonshotAI/kimi-code/commit/ee69e16dc8fb18153d7ddff04bef1f4fc593688a) - Fix MCP server working directories when sessions are hosted by the web server. + +- [#1064](https://github.com/MoonshotAI/kimi-code/pull/1064) [`a752a53`](https://github.com/MoonshotAI/kimi-code/commit/a752a5309b3c456f7da0e6141bcd435b497d127a) - Fix truncated skill descriptions missing an ellipsis in the model's skill listing. + +- [#903](https://github.com/MoonshotAI/kimi-code/pull/903) [`bbd8a1a`](https://github.com/MoonshotAI/kimi-code/commit/bbd8a1a947ba26c0e59f98819cab9e20898ff0b7) - Fix `kimi web` and `/web` failing to start the background server daemon on Windows with `spawn EFTYPE` when the CLI is installed via npm/pnpm or run from source. The official single-binary install script was not affected. + +- [#1070](https://github.com/MoonshotAI/kimi-code/pull/1070) [`ff17715`](https://github.com/MoonshotAI/kimi-code/commit/ff177155ca630248bcd692421faab21e7b5be069) - Stop auto-dismissing questions in the web UI after 60 seconds so they wait for the user's answer. + +- [#1097](https://github.com/MoonshotAI/kimi-code/pull/1097) [`27ef516`](https://github.com/MoonshotAI/kimi-code/commit/27ef5166955b5deaecc367a4b3393909b0ccc9f9) - Add a hint to the per-turn step limit error pointing users to the loop_control.max_steps_per_turn config option. + +- [#1062](https://github.com/MoonshotAI/kimi-code/pull/1062) [`ea6a4bf`](https://github.com/MoonshotAI/kimi-code/commit/ea6a4bfe6ef8914f67f254f24b0c5c543c48a341) - Preserve full tool output logs when previews are truncated and link background task completion notifications to saved output. + +- [#1086](https://github.com/MoonshotAI/kimi-code/pull/1086) [`fe667d7`](https://github.com/MoonshotAI/kimi-code/commit/fe667d7c2ef113aef8a9546148f980d9adf560a3) - `/reload` now refreshes the assistant's view of plugin skills, so plugin changes take effect in the current session instead of requiring a new one. + +- [#1081](https://github.com/MoonshotAI/kimi-code/pull/1081) [`8fc6aa5`](https://github.com/MoonshotAI/kimi-code/commit/8fc6aa5f6842aa78acf8f23912342b721efcf7a9) - Sync session title changes across all connected clients in server mode. + +- [#1078](https://github.com/MoonshotAI/kimi-code/pull/1078) [`75ca3b2`](https://github.com/MoonshotAI/kimi-code/commit/75ca3b21609d7197bb2c9b4389901595840ac7e3) - Add Ctrl+U and Ctrl+D as page up and page down shortcuts in the task output viewer. + +- [#1069](https://github.com/MoonshotAI/kimi-code/pull/1069) [`d18aa16`](https://github.com/MoonshotAI/kimi-code/commit/d18aa1666a09b038d5a107e9a37fff1031b2e847) - Reduce streaming redraw cost for long assistant messages with code blocks. + +- [#1112](https://github.com/MoonshotAI/kimi-code/pull/1112) [`6a97d0b`](https://github.com/MoonshotAI/kimi-code/commit/6a97d0bf431bc7038ce801da21164a67e07422d8) - Add a copy button to user messages in the web chat. + +- [#1035](https://github.com/MoonshotAI/kimi-code/pull/1035) [`ea03f30`](https://github.com/MoonshotAI/kimi-code/commit/ea03f30e5174825049ed4dfedebf8e43fbe751a4) - Render LaTeX display math (`$$…$$`) in the web chat via KaTeX. Single `$` is intentionally left as literal text, so prices, env vars, and shell paths (e.g. `$PATH`, `$5/$10`, `$HOME/bin`) are never swallowed as a formula. + +- [#1084](https://github.com/MoonshotAI/kimi-code/pull/1084) [`d6e5246`](https://github.com/MoonshotAI/kimi-code/commit/d6e524682d9fb95460fceb86e17632ed858f7fcb) - Page the web session list per workspace so the first screen no longer fetches every session up front. + +- [#1113](https://github.com/MoonshotAI/kimi-code/pull/1113) [`6194d3f`](https://github.com/MoonshotAI/kimi-code/commit/6194d3fad3b53e6c2b80c422fe98043145494655) - Keep the web session sidebar from re-rendering on every streaming token. The + event reducer now reuses the `sessions` array reference for events that do not + change sessions, so the sidebar computeds (`sessionsForView` / `workspaceGroups` + / `mergedWorkspaces`) are no longer dirtied by unrelated high-frequency events. + +- [#1087](https://github.com/MoonshotAI/kimi-code/pull/1087) [`884b65a`](https://github.com/MoonshotAI/kimi-code/commit/884b65a04014be8d68ffd406f89fc2d26af6e62c) - Fix duplicate session snapshot reloads in the bundled web UI during resync. + +- [#1109](https://github.com/MoonshotAI/kimi-code/pull/1109) [`d554f9a`](https://github.com/MoonshotAI/kimi-code/commit/d554f9ac8771be09b5c9a56943167dd45108dc4f) - Show the full accumulated progress of a subagent in its detail panel, with concise tool-call summaries instead of raw JSON. + +- [#1065](https://github.com/MoonshotAI/kimi-code/pull/1065) [`4b837d6`](https://github.com/MoonshotAI/kimi-code/commit/4b837d6bfbf3850807b5f88ccdd10f31e69b019c) - Create missing parent directories automatically when writing a file. + +## 0.19.2 + +### Patch Changes + +- [#999](https://github.com/MoonshotAI/kimi-code/pull/999) [`6b68aa8`](https://github.com/MoonshotAI/kimi-code/commit/6b68aa85e2a58cfdaacba5580f66a6a74550ccf6) - Add `-c` as a shorthand for `--continue`. + +- [#1028](https://github.com/MoonshotAI/kimi-code/pull/1028) [`be77d5d`](https://github.com/MoonshotAI/kimi-code/commit/be77d5da03b96ebc24169ef563be1dc1c545590f) - Show a transient footer hint when an image is detected in the clipboard, displaying the platform-appropriate paste shortcut. + +- [#1004](https://github.com/MoonshotAI/kimi-code/pull/1004) [`d70c3a8`](https://github.com/MoonshotAI/kimi-code/commit/d70c3a8c0121f55e5f29f9a2ad01b17df449467a) - Show the command in running Bash tool cards and allow expanding it with Ctrl+O before the result arrives. + +- [#1009](https://github.com/MoonshotAI/kimi-code/pull/1009) [`e47de61`](https://github.com/MoonshotAI/kimi-code/commit/e47de610e4de9b11ccd182c0c16387f9d3fb0de4) - Add a Ctrl+T shortcut to expand and collapse a truncated todo list. + +- [#1028](https://github.com/MoonshotAI/kimi-code/pull/1028) [`be77d5d`](https://github.com/MoonshotAI/kimi-code/commit/be77d5da03b96ebc24169ef563be1dc1c545590f) - Fix stale rows occasionally leaving duplicate input boxes after tall content shrinks. + +- [#1028](https://github.com/MoonshotAI/kimi-code/pull/1028) [`be77d5d`](https://github.com/MoonshotAI/kimi-code/commit/be77d5da03b96ebc24169ef563be1dc1c545590f) - Fix inline images being rendered as broken escape sequences in the transcript. + +- [#1027](https://github.com/MoonshotAI/kimi-code/pull/1027) [`c240bfa`](https://github.com/MoonshotAI/kimi-code/commit/c240bfab7d2b00d41b993f681be612f2db45baa7) - Fix resume not realigning a tool call that was interrupted mid-history. The synthetic interrupted result is now closed in place at the next step boundary, so later turns and deferred messages keep their recorded order instead of only the trailing exchange being repaired. The `/messages` wire transcript reducer mirrors the same closure so its folded length stays aligned with live history, preventing the later turn from being duplicated/reordered. Replay also drops a tool result whose call is no longer awaiting one, so a stale interrupted result left at the log tail by an older resume of a damaged session is not re-applied as a duplicate. + +- [#1012](https://github.com/MoonshotAI/kimi-code/pull/1012) [`fd16ffb`](https://github.com/MoonshotAI/kimi-code/commit/fd16ffb80a90fda8a611a27a158b9b7a33e13303) - Show subcommand suggestions after Tab-completing a slash command name. + +- [#1012](https://github.com/MoonshotAI/kimi-code/pull/1012) [`fd16ffb`](https://github.com/MoonshotAI/kimi-code/commit/fd16ffb80a90fda8a611a27a158b9b7a33e13303) - Fix the Tab key unexpectedly opening the file completion list. + +- [#1044](https://github.com/MoonshotAI/kimi-code/pull/1044) [`9d197e0`](https://github.com/MoonshotAI/kimi-code/commit/9d197e0f67c879306b9d7659d66e9295e63faa5a) - Fix clipboard copy actions in the web UI when served over plain HTTP. + +- [#1032](https://github.com/MoonshotAI/kimi-code/pull/1032) [`a753b05`](https://github.com/MoonshotAI/kimi-code/commit/a753b0535e44f624289715bd560cf1346149e786) - Fix code blocks nested inside list items rendering blank in the web chat after a turn finishes generating. + +- [#1015](https://github.com/MoonshotAI/kimi-code/pull/1015) [`83384ee`](https://github.com/MoonshotAI/kimi-code/commit/83384ee6d46b37c00b7b8f160a7c48aebbd6921e) - Fix the composer's ↑/↓ input-history recall doing nothing right after the first message of a new session. The history is now persisted to localStorage and re-read on mount, so the docked composer no longer starts empty when it takes over from the empty-session composer. Slash commands are now recorded too — both typed-and-submitted and ones picked from the slash menu — so they can be recalled like plain messages. + +- [#1003](https://github.com/MoonshotAI/kimi-code/pull/1003) [`e15edfd`](https://github.com/MoonshotAI/kimi-code/commit/e15edfd017506fde396b8b0dcf68008b61b39752) - Fix the web question prompt missing the free-text Other option. + +- [#1056](https://github.com/MoonshotAI/kimi-code/pull/1056) [`b93e936`](https://github.com/MoonshotAI/kimi-code/commit/b93e9365b68d53f8f1a148e7349a5865b1b669a8) - Fix yolo mode in the web app auto-approving plan reviews and sensitive file access. + +- [#971](https://github.com/MoonshotAI/kimi-code/pull/971) [`b84704b`](https://github.com/MoonshotAI/kimi-code/commit/b84704bff39ae5cb382d2a8dc0911db286e84ead) - Read large text files in bounded memory and read tail lines without scanning whole files. + +- [#1020](https://github.com/MoonshotAI/kimi-code/pull/1020) [`9c553e4`](https://github.com/MoonshotAI/kimi-code/commit/9c553e4bf7d0a2c09030212fe06577343ea76a60) - Add an Alt+S shortcut in the model picker to switch the model for the current session only, without saving it as the default. + +- [#1043](https://github.com/MoonshotAI/kimi-code/pull/1043) [`27df39c`](https://github.com/MoonshotAI/kimi-code/commit/27df39c7ed2b012815c380a33fe56bd37c7fc7c1) - Fix web chat stop actions so stale prompt ids fall back to cancelling the active session. + +- [#1036](https://github.com/MoonshotAI/kimi-code/pull/1036) [`866b91c`](https://github.com/MoonshotAI/kimi-code/commit/866b91c8f5dc98dfc18e5c658beaa11afea5032e) - Reorganize the web app's components into area subdirectories (chat/settings/dialogs/mobile) and refresh the component path comments. + +- [#1042](https://github.com/MoonshotAI/kimi-code/pull/1042) [`dc6b9ef`](https://github.com/MoonshotAI/kimi-code/commit/dc6b9ef02bf7583d166c8c5b001a960329c225f8) - Add a development-mode indicator to the web sidebar for local development. + +- [#1047](https://github.com/MoonshotAI/kimi-code/pull/1047) [`98d3e5b`](https://github.com/MoonshotAI/kimi-code/commit/98d3e5b71d5760475f7a5a23b2b794584d12b89b) - Keep the web sidebar's workspace order stable and let workspaces be reordered by drag-and-drop, persisted locally instead of following recent activity; sessions now also float to the top of their group as soon as a new message arrives. + +- [#1034](https://github.com/MoonshotAI/kimi-code/pull/1034) [`603a767`](https://github.com/MoonshotAI/kimi-code/commit/603a7679de91e221802a7f7b0ab7df23c7e5526c) - Extract the composer's image/video attachment handling into a reusable composable. + +- [#1031](https://github.com/MoonshotAI/kimi-code/pull/1031) [`2bfd686`](https://github.com/MoonshotAI/kimi-code/commit/2bfd6860e487f902be53fd5f52f03e66d1839ae2) - Extract the composer's text state and per-session draft persistence into a reusable composable. + +- [#1011](https://github.com/MoonshotAI/kimi-code/pull/1011) [`fb780fc`](https://github.com/MoonshotAI/kimi-code/commit/fb780fce9665e2119cee6d0bc7f85895c6970865) - Extract the composer's shell-style input-history recall into a reusable composable. + +- [#1030](https://github.com/MoonshotAI/kimi-code/pull/1030) [`661c1fb`](https://github.com/MoonshotAI/kimi-code/commit/661c1fbe5b026ec32d80696290a18313b24eafef) - Extract the composer's @-mention menu logic into a reusable composable. + +- [#1026](https://github.com/MoonshotAI/kimi-code/pull/1026) [`318c964`](https://github.com/MoonshotAI/kimi-code/commit/318c964f074123ad228cbddcf7809fa4baaa7fb2) - Extract the composer's slash-command menu logic into a reusable composable. + +- [#1045](https://github.com/MoonshotAI/kimi-code/pull/1045) [`ac1882f`](https://github.com/MoonshotAI/kimi-code/commit/ac1882fe28c906904ffaacd8434bb20e689d6677) - Persist the collapsed state of workspace groups in the web sidebar across page reloads. + +- [#1001](https://github.com/MoonshotAI/kimi-code/pull/1001) [`ea1b33b`](https://github.com/MoonshotAI/kimi-code/commit/ea1b33b6743b822aa5083dbeb2d5e84a78b0ab3d) - Extract pure turn-rendering helpers out of the chat pane into their own module. + +- [#1010](https://github.com/MoonshotAI/kimi-code/pull/1010) [`a2650f8`](https://github.com/MoonshotAI/kimi-code/commit/a2650f85d467707e7c85d22cff590f68852d33f3) - Extract the beta conversation outline (table of contents) into its own component. + +- [#998](https://github.com/MoonshotAI/kimi-code/pull/998) [`3e4793d`](https://github.com/MoonshotAI/kimi-code/commit/3e4793d6111059cbfb97159f682ed4bd7a33441d) - Extract the workspace group rendering out of the sidebar into its own component. + +- [#985](https://github.com/MoonshotAI/kimi-code/pull/985) [`92c2cf0`](https://github.com/MoonshotAI/kimi-code/commit/92c2cf0ef57f00928d337bcfeb1d7eff9b0d0f7f) - Allow the web sidebar and detail panel to be resized up to the available viewport width, keeping their resize handles reachable on narrow windows. + +- [#1033](https://github.com/MoonshotAI/kimi-code/pull/1033) [`b1e6b64`](https://github.com/MoonshotAI/kimi-code/commit/b1e6b6431903fde002fdddbdfcabfab39f3ef5c5) - Optimize the loading tips display. + +## 0.19.1 + +### Patch Changes + +- [#992](https://github.com/MoonshotAI/kimi-code/pull/992) [`7341fb4`](https://github.com/MoonshotAI/kimi-code/commit/7341fb4979523d4429ccf9177b5e3907f544d8c0) - Fix ACP editors such as Zed failing to start a new thread. + +- [#984](https://github.com/MoonshotAI/kimi-code/pull/984) [`da81858`](https://github.com/MoonshotAI/kimi-code/commit/da81858802127cb8bb8ed2deaa1989793b356adf) - Clear all per-session state when a session is archived or removed, so archived sessions no longer leave orphaned data behind. + +- [#978](https://github.com/MoonshotAI/kimi-code/pull/978) [`d4ae02d`](https://github.com/MoonshotAI/kimi-code/commit/d4ae02d82e9da0d163ea4235a54d6535c591172e) - Fix the web sidebar's unread dots getting out of sync across browser tabs. + +- [#979](https://github.com/MoonshotAI/kimi-code/pull/979) [`8c6cade`](https://github.com/MoonshotAI/kimi-code/commit/8c6cade69efa42fdcc280f51a283ea6f717d62fc) - Consolidate web client localStorage access and split the root state store and app shell into focused composables. + +## 0.19.0 + +### Minor Changes + +- [#812](https://github.com/MoonshotAI/kimi-code/pull/812) [`c0eeca2`](https://github.com/MoonshotAI/kimi-code/commit/c0eeca24692edd736eecd3c2541d7566bac9f80f) - Added the ability to add extra workspace directories: + + - Use the `/add-dir <path>` command to add extra working directories to the current session, or remember them for the project. + - Use `kimi --add-dir <path>` to add them on startup. + - Project-level local config is now managed in `.kimi-code/local.toml`; we recommend adding it to your `.gitignore`. + +- [#975](https://github.com/MoonshotAI/kimi-code/pull/975) [`c5c1834`](https://github.com/MoonshotAI/kimi-code/commit/c5c18347251221fab74e4f452ac4910116c4224d) - Speed up session snapshot loading with a direct disk reader and a request timeout safeguard, keeping the previous path as a legacy fallback. + +### Patch Changes + +- [#910](https://github.com/MoonshotAI/kimi-code/pull/910) [`7644f10`](https://github.com/MoonshotAI/kimi-code/commit/7644f1036ca1079e4527c0b1c825ec5384d6d8da) - Fix provider requests failing when restored conversation history contains empty text content blocks. + +- [#963](https://github.com/MoonshotAI/kimi-code/pull/963) [`4292ae9`](https://github.com/MoonshotAI/kimi-code/commit/4292ae9f9bc49e9edaaaeae50dbddabbd4b9bb25) - Surface provider safety-policy blocks instead of silently treating them as completed turns, and prevent the context token count from dropping to zero after a filtered response. + +- [#970](https://github.com/MoonshotAI/kimi-code/pull/970) [`2730079`](https://github.com/MoonshotAI/kimi-code/commit/27300797f2149900219b05dda49dce65e71fa85a) - Detect the real image format from file contents when reading media, so a mismatched filename extension no longer produces a data URL the model API rejects. + +- [#977](https://github.com/MoonshotAI/kimi-code/pull/977) [`d521932`](https://github.com/MoonshotAI/kimi-code/commit/d521932c3e99a0c5fa1d5d658cf1cd64f0306a75) - Stop showing unread dots on cancelled or failed sessions in the web sidebar. + +- [#957](https://github.com/MoonshotAI/kimi-code/pull/957) [`b57fc90`](https://github.com/MoonshotAI/kimi-code/commit/b57fc905fe480aac07839dd0213768dbeb2a8002) - Fix commands flashing an empty console window on Windows. + +- [#821](https://github.com/MoonshotAI/kimi-code/pull/821) [`ba64072`](https://github.com/MoonshotAI/kimi-code/commit/ba64072559c1e9bb3447ede39991ac2e8bdb7645) - Allow long-running foreground commands and subagents to be moved into background tasks with Ctrl+B, and inspect them via the `/tasks` panel. + +- [#812](https://github.com/MoonshotAI/kimi-code/pull/812) [`c0eeca2`](https://github.com/MoonshotAI/kimi-code/commit/c0eeca24692edd736eecd3c2541d7566bac9f80f) - Polish file mention UX. + +- [#974](https://github.com/MoonshotAI/kimi-code/pull/974) [`d434d8f`](https://github.com/MoonshotAI/kimi-code/commit/d434d8f0d809599f4ae7de77b58e337bfd4ebcc9) - Unify image format detection when sniffing fails. + +- [#958](https://github.com/MoonshotAI/kimi-code/pull/958) [`98905eb`](https://github.com/MoonshotAI/kimi-code/commit/98905eb409ec643fd916a13beecec85212f834bd) - Show longer branch names in the web chat header and expose the full name on hover. + +- [#964](https://github.com/MoonshotAI/kimi-code/pull/964) [`4223739`](https://github.com/MoonshotAI/kimi-code/commit/42237392ddc3a0816c045da23e77c4875cc692e5) - Keep the web page title fixed instead of changing with the session or workspace name. + +- [#973](https://github.com/MoonshotAI/kimi-code/pull/973) [`3b9938b`](https://github.com/MoonshotAI/kimi-code/commit/3b9938b4c3a386394ed4d35c7b89b48878476977) - Consolidate web client localStorage access and decouple appearance/notification state into dedicated modules. + +## 0.18.0 + +### Minor Changes + +- [#888](https://github.com/MoonshotAI/kimi-code/pull/888) [`58898de`](https://github.com/MoonshotAI/kimi-code/commit/58898de0200d6626ca634e344fe85b860abcfd1b) - Add an environment variable to cap AgentSwarm concurrency during the initial ramp, so large swarms do not trip provider rate limits as easily. + +- [#895](https://github.com/MoonshotAI/kimi-code/pull/895) [`495fe8c`](https://github.com/MoonshotAI/kimi-code/commit/495fe8c674d654cdf87217ca4ada775507f861f6) - Add instant session search to the web sidebar, filtering by title and the last user prompt. + +### Patch Changes + +- [#896](https://github.com/MoonshotAI/kimi-code/pull/896) [`de610de`](https://github.com/MoonshotAI/kimi-code/commit/de610deb5f760606b82cc595e59c5176cc66ce82) - Fix the web workspace session count so it drops to 0 after archiving the last session instead of staying at 1. + +- [#876](https://github.com/MoonshotAI/kimi-code/pull/876) [`49183d8`](https://github.com/MoonshotAI/kimi-code/commit/49183d8729e3e7d361a253dc5c68f409e6382ba9) - Suggest `/reload` alongside `/new` in plugin-change hints. + +- [#867](https://github.com/MoonshotAI/kimi-code/pull/867) [`d1dc2a3`](https://github.com/MoonshotAI/kimi-code/commit/d1dc2a3e77ec1422d60cb008c5520a44a2ed7c00) - Redesign the web OAuth login dialog: lead with a single "Authorize in browser" button that opens the verification link with the device code already embedded, demote manual code entry to a clearly secondary fallback, and drop the duplicate open-browser and cancel controls so the order of steps is unambiguous. + +- [#867](https://github.com/MoonshotAI/kimi-code/pull/867) [`d1dc2a3`](https://github.com/MoonshotAI/kimi-code/commit/d1dc2a3e77ec1422d60cb008c5520a44a2ed7c00) - Fix the web login slash command description to match the browser authorization flow. + +- [#893](https://github.com/MoonshotAI/kimi-code/pull/893) [`d7ec056`](https://github.com/MoonshotAI/kimi-code/commit/d7ec05686a09580f9ffd99f6ef26385aed8eb02c) - Add scroll-up lazy loading for older messages in the web chat session view, and fix the "new messages" pill overlapping the composer dock. + +- [#882](https://github.com/MoonshotAI/kimi-code/pull/882) [`8ab9e96`](https://github.com/MoonshotAI/kimi-code/commit/8ab9e969637ffee18b09a0b265ffa860c5a2e11c) - Fix the web app only loading the 20 most recent sessions; it now follows pagination so older sessions are reachable. + +- [#889](https://github.com/MoonshotAI/kimi-code/pull/889) [`23277a5`](https://github.com/MoonshotAI/kimi-code/commit/23277a574c7e0782c04f62e10370494247be3a66) - Show the connected server version in the web settings General tab. + +- [#881](https://github.com/MoonshotAI/kimi-code/pull/881) [`7bc3d99`](https://github.com/MoonshotAI/kimi-code/commit/7bc3d99933b0bbc3f9188a2b02bcc90e81623f72) - Keep the highlighted web slash command visible while navigating a long slash menu. + +- [#878](https://github.com/MoonshotAI/kimi-code/pull/878) [`a74a6b7`](https://github.com/MoonshotAI/kimi-code/commit/a74a6b7f6b1d13d24eae356a2208c012128b180d) - Allow long web slash command names and descriptions to wrap without overflowing the slash menu. + +- [#878](https://github.com/MoonshotAI/kimi-code/pull/878) [`a74a6b7`](https://github.com/MoonshotAI/kimi-code/commit/a74a6b7f6b1d13d24eae356a2208c012128b180d) - Fix web slash skill selection sending immediately and allow slash search to match skill names by substring. + +## 0.17.1 + +### Patch Changes + +- [#861](https://github.com/MoonshotAI/kimi-code/pull/861) [`bd09795`](https://github.com/MoonshotAI/kimi-code/commit/bd0979578bcad5fe3bf989e022b7823824f3f25c) - Prevent the web login dialog from closing when clicking the backdrop. + +- [#860](https://github.com/MoonshotAI/kimi-code/pull/860) [`0e2877b`](https://github.com/MoonshotAI/kimi-code/commit/0e2877bee347466ed6cc8afda9f9faf338069012) - Stop the background local server from locking the directory it was started in. + +- [#860](https://github.com/MoonshotAI/kimi-code/pull/860) [`0e2877b`](https://github.com/MoonshotAI/kimi-code/commit/0e2877bee347466ed6cc8afda9f9faf338069012) - Fix the local server failing to start in the background on the native binary. + +- [#861](https://github.com/MoonshotAI/kimi-code/pull/861) [`bd09795`](https://github.com/MoonshotAI/kimi-code/commit/bd0979578bcad5fe3bf989e022b7823824f3f25c) - Group the default model dropdown in web settings by provider. + +## 0.17.0 + +### Minor Changes + +- [#625](https://github.com/MoonshotAI/kimi-code/pull/625) [`9a8fea5`](https://github.com/MoonshotAI/kimi-code/commit/9a8fea5c85177cd887896108c05ba9e174f28250) - Add the server-hosted web UI and the CLI commands that power it: + + - `kimi server` to start, stop, and manage the local server. + - `kimi web` to open the server-hosted web UI in a browser. + - Server REST and WebSocket APIs for the web client. + - Web chat layout, session list, auto-scroll, and related behaviors. + +### Patch Changes + +- [#838](https://github.com/MoonshotAI/kimi-code/pull/838) [`843a731`](https://github.com/MoonshotAI/kimi-code/commit/843a731097fc18b2e41ab0405b5fbcb6149ba55c) - Show the underlying connection error when OAuth token refresh fails after internal retries, instead of prompting for login. Token refresh failures are no longer re-retried at the agent loop level. + +- [#849](https://github.com/MoonshotAI/kimi-code/pull/849) [`254f946`](https://github.com/MoonshotAI/kimi-code/commit/254f946a506b01df7a559ed63bd8d705e9fa7496) - Skip debug TPS when the output stream is too short to measure reliably. + +- [#833](https://github.com/MoonshotAI/kimi-code/pull/833) [`a71b2e3`](https://github.com/MoonshotAI/kimi-code/commit/a71b2e3123ff8454f725b3d24e8c985608c5c4f9) - Restore the turn counter from persisted loop events on resume so post-resume turns no longer reuse turn ids that already appear in history. + +- [#853](https://github.com/MoonshotAI/kimi-code/pull/853) [`05fe759`](https://github.com/MoonshotAI/kimi-code/commit/05fe7595ab9bac8230fd9f2fe7bdbaaa157ddc9b) - Fix the web login page and no-workspace conversation startup flow. + +## 0.16.0 + +### Minor Changes + +- [#788](https://github.com/MoonshotAI/kimi-code/pull/788) [`efdf8a1`](https://github.com/MoonshotAI/kimi-code/commit/efdf8a1b2d4e906fbb35620083c3e7b490e0e88a) - Add a built-in `kimi vis` command that launches the session visualizer in your browser, pointed at your local sessions. Supports `--port`/`--host`, `--no-open`, and `kimi vis <sessionId>` deep-links. + +### Patch Changes + +- [#790](https://github.com/MoonshotAI/kimi-code/pull/790) [`d0d5821`](https://github.com/MoonshotAI/kimi-code/commit/d0d58219007cd9d7355f1ea8900e9777b66abda2) - Stop Anthropic-compatible providers from reading ambient Anthropic shell credentials and custom headers. + +- [#809](https://github.com/MoonshotAI/kimi-code/pull/809) [`6f442bd`](https://github.com/MoonshotAI/kimi-code/commit/6f442bd8cde29e21526fa36c9836e2d4c282b4bf) - Add configurable banner display frequencies with local display state. + +- [#807](https://github.com/MoonshotAI/kimi-code/pull/807) [`b45672c`](https://github.com/MoonshotAI/kimi-code/commit/b45672cdaac9959024c3ae36bf35b16a423aa1dc) - Close wrapped output streams when buffered readers are destroyed. + +- [#813](https://github.com/MoonshotAI/kimi-code/pull/813) [`7b5b818`](https://github.com/MoonshotAI/kimi-code/commit/7b5b8188157ec902e5cd4e73545bc5ca6c52bb76) - Fix repeated compaction handling when context remains over the blocking threshold. + +- [#801](https://github.com/MoonshotAI/kimi-code/pull/801) [`ff332be`](https://github.com/MoonshotAI/kimi-code/commit/ff332be6d364ce3d5974133deb7c76220684181a) - Polish queue pane styling + +- [#802](https://github.com/MoonshotAI/kimi-code/pull/802) [`aa1896c`](https://github.com/MoonshotAI/kimi-code/commit/aa1896ca749e41a67d7c4b655dcc8be830cbec82) - Reduce the maximum height of the /btw side panel from half to one-third of the terminal. + +- [#805](https://github.com/MoonshotAI/kimi-code/pull/805) [`3e6196e`](https://github.com/MoonshotAI/kimi-code/commit/3e6196e6b227c66860651f4335e06973865b2714) - Project session replay ranges over rendered replay records instead of raw persisted records. + +- [#804](https://github.com/MoonshotAI/kimi-code/pull/804) [`299b9fc`](https://github.com/MoonshotAI/kimi-code/commit/299b9fcad4c9c4b755fae4dfae01a1dbf60aec3c) - Prevent session shutdown from resuming the agent when stopping background tasks. + +- [#823](https://github.com/MoonshotAI/kimi-code/pull/823) [`90fc04b`](https://github.com/MoonshotAI/kimi-code/commit/90fc04b7072ec20055022c50583d35286ca715a6) - Remove redundant LLM request logging context plumbing. + +## 0.15.0 + +### Minor Changes + +- [#779](https://github.com/MoonshotAI/kimi-code/pull/779) [`2746c71`](https://github.com/MoonshotAI/kimi-code/commit/2746c71c47058d9a3bb73e27a07ebfcf44bf4119) - Add an all-sessions picker view with name search, paginated browsing, and clipboard-ready resume commands for sessions in other working directories. + +- [#744](https://github.com/MoonshotAI/kimi-code/pull/744) [`18f299f`](https://github.com/MoonshotAI/kimi-code/commit/18f299fd0b266545a1f7cebae9f58b83b9d9776e) - Add support for legacy SSE MCP servers alongside stdio and streamable HTTP transports. + +### Patch Changes + +- [#777](https://github.com/MoonshotAI/kimi-code/pull/777) [`4516f62`](https://github.com/MoonshotAI/kimi-code/commit/4516f62f6a7e4dd7675a3aec16b2a26c5e310d83) - Clarify AGENTS.md prompt guidance and mark truncated instruction files. + +- [#780](https://github.com/MoonshotAI/kimi-code/pull/780) [`8a92db6`](https://github.com/MoonshotAI/kimi-code/commit/8a92db6a0c110a21c6e6e86622f498e836178e5f) - Prompt the CLI to show one brief same-language status sentence before non-trivial tool calls. + +- [#786](https://github.com/MoonshotAI/kimi-code/pull/786) [`e10b25f`](https://github.com/MoonshotAI/kimi-code/commit/e10b25f9be18ca64aada0d0a3cab0e02fdbd46df) - Stop writing resume version markers into persisted agent metadata. + +- [#768](https://github.com/MoonshotAI/kimi-code/pull/768) [`c6a9967`](https://github.com/MoonshotAI/kimi-code/commit/c6a996756cd8f1fb317b6eee6f4e668eebc7dc14) - Recover resumed sessions when an interrupted tool call result was not recorded. + +- [#775](https://github.com/MoonshotAI/kimi-code/pull/775) [`3fa1b8e`](https://github.com/MoonshotAI/kimi-code/commit/3fa1b8ea7deb558b88073b5f7b02857e52c3f60c) - Optimize the npm packaging system. + +- [#343](https://github.com/MoonshotAI/kimi-code/pull/343) [`73be7ba`](https://github.com/MoonshotAI/kimi-code/commit/73be7ba17d41df7999d4c1fba410994e7024eb7b) - Repair mismatched JSON Schema types emitted by Xcode 26.5 MCP server for Moonshot compatibility. + +- [#777](https://github.com/MoonshotAI/kimi-code/pull/777) [`4516f62`](https://github.com/MoonshotAI/kimi-code/commit/4516f62f6a7e4dd7675a3aec16b2a26c5e310d83) - Collapse hidden directories in the workspace prompt and explain how to inspect them. + +- [#766](https://github.com/MoonshotAI/kimi-code/pull/766) [`9cef896`](https://github.com/MoonshotAI/kimi-code/commit/9cef89656311974a57e6675f474ea6c2adb1d8e9) - Clarify that compaction summaries must be emitted in the final answer. + +- [#765](https://github.com/MoonshotAI/kimi-code/pull/765) [`046856b`](https://github.com/MoonshotAI/kimi-code/commit/046856b740afb604132e914f1fc489de72394036) - Read media files using header-detected types before falling back to media extensions. + +- [#779](https://github.com/MoonshotAI/kimi-code/pull/779) [`2746c71`](https://github.com/MoonshotAI/kimi-code/commit/2746c71c47058d9a3bb73e27a07ebfcf44bf4119) - Show the all-sessions toggle hint when the current working directory has no sessions. + +- [#785](https://github.com/MoonshotAI/kimi-code/pull/785) [`4578f05`](https://github.com/MoonshotAI/kimi-code/commit/4578f05f44101f24d45c6452e2a6993cbb52e331) - Include the skill's directory on the loaded-skill context block so the agent can locate a skill's bundled resources (scripts, templates) after it is invoked. + +- [#784](https://github.com/MoonshotAI/kimi-code/pull/784) [`a562ef5`](https://github.com/MoonshotAI/kimi-code/commit/a562ef54e537a36211c48f0fe19e9252e83397a0) - Decouple agent skill access from session-specific registry implementations. + +- [#772](https://github.com/MoonshotAI/kimi-code/pull/772) [`d47e699`](https://github.com/MoonshotAI/kimi-code/commit/d47e699015f02f4f76723aa8fb17d51a74aa74ff) - Do not carry obsolete legacy loop, background, plan, yolo, or unknown experimental flags into migrated config files. + +- [#783](https://github.com/MoonshotAI/kimi-code/pull/783) [`e2a407c`](https://github.com/MoonshotAI/kimi-code/commit/e2a407ce31685220b2f891a7f6d8b89c62418c98) - Keep TUI components within narrow terminal widths by wrapping, compacting, or truncating lines that could exceed the render width. + +- [#776](https://github.com/MoonshotAI/kimi-code/pull/776) [`ecd7a0a`](https://github.com/MoonshotAI/kimi-code/commit/ecd7a0afb646d14a14c780a4088fd8a59da134ad) - Resolve model capabilities through a static lookup instead of instantiating a temporary provider. + +- [#767](https://github.com/MoonshotAI/kimi-code/pull/767) [`a355f2a`](https://github.com/MoonshotAI/kimi-code/commit/a355f2af2fd68ad9e2bdc72ce854cd18c8242ce8) - Prioritize clearing draft editor text before Ctrl-C cancels an active stream. + +- [#787](https://github.com/MoonshotAI/kimi-code/pull/787) [`1eb363f`](https://github.com/MoonshotAI/kimi-code/commit/1eb363f655aa44abc1e5c3af89016f00764ecc95) - Extend the same-language rule to the model's reasoning, so thinking follows the user's language while keeping code and technical terms in their original form. + +## 0.14.3 + +### Patch Changes + +- [#713](https://github.com/MoonshotAI/kimi-code/pull/713) [`f874251`](https://github.com/MoonshotAI/kimi-code/commit/f874251288927243a9b9d4bfd546e8c17754d566) - Refresh provider model metadata before opening the model picker. + +## 0.14.2 + +### Patch Changes + +- [#683](https://github.com/MoonshotAI/kimi-code/pull/683) [`ad239cb`](https://github.com/MoonshotAI/kimi-code/commit/ad239cb1c08266a442c9ca0382fefed87bcb1fd4) - Allow `--auto`, `--yolo`, and `--plan` to be combined with `--session` or `--continue` by applying the requested mode to the resumed session. + +- [#690](https://github.com/MoonshotAI/kimi-code/pull/690) [`7f0dde2`](https://github.com/MoonshotAI/kimi-code/commit/7f0dde2ece3f9a004e934d69258dfd47c954043c) - Fix endless desktop notifications in iTerm2 by only sending terminal progress sequences to terminals that support them. + +- [#651](https://github.com/MoonshotAI/kimi-code/pull/651) [`c39c625`](https://github.com/MoonshotAI/kimi-code/commit/c39c62590db708fc81bd8627ea661c38f3fff9af) - Qualify sub-skill names with their parent prefix and expose sub-skills as dotted slash commands in the TUI. + +- [#617](https://github.com/MoonshotAI/kimi-code/pull/617) [`911e7c3`](https://github.com/MoonshotAI/kimi-code/commit/911e7c3fcfc8a005b1b8d90388260d1a4032f76f) - Show completed and cancelled compaction records correctly when resuming a session. + +- [#676](https://github.com/MoonshotAI/kimi-code/pull/676) [`dcf3075`](https://github.com/MoonshotAI/kimi-code/commit/dcf30754d09c7560101bc410387792194c3fe2b4) - Stream foreground Bash stdout and stderr while commands are still running. + +- [#692](https://github.com/MoonshotAI/kimi-code/pull/692) [`7ca9bdf`](https://github.com/MoonshotAI/kimi-code/commit/7ca9bdfed516d148b063229a9686a28f9e29aaef) - Skip re-entering plan mode when resuming a session that is already in plan mode (previously failed with "Already in plan mode"), and stop re-applying `--auto`/`--yolo`/`--plan` startup flags when switching sessions through the `/sessions` picker. + +- [#675](https://github.com/MoonshotAI/kimi-code/pull/675) [`d1ba145`](https://github.com/MoonshotAI/kimi-code/commit/d1ba14562bafdb6b93c3eec1b5c453186507ed56) - Sync custom registry provider additions, removals, and rotated registry keys during startup refresh. + +- [#689](https://github.com/MoonshotAI/kimi-code/pull/689) [`8d251f8`](https://github.com/MoonshotAI/kimi-code/commit/8d251f8ab44ead65f6c1bb264980ee7d075142ad) - Drop invalid config.toml sections with a warning instead of failing to start. + +## 0.14.1 + +### Patch Changes + +- [#643](https://github.com/MoonshotAI/kimi-code/pull/643) [`4e5043b`](https://github.com/MoonshotAI/kimi-code/commit/4e5043b03b2fb03374550dc65d04871bc83e932a) - Require AgentSwarm tool calls to run alone in a model response. + +- [#631](https://github.com/MoonshotAI/kimi-code/pull/631) [`2961425`](https://github.com/MoonshotAI/kimi-code/commit/296142544ec64e93c9083a51d3a53a83496d10cb) - Wrap long command and skill descriptions in the autocomplete menu onto a second line instead of cutting them off. + +- [#661](https://github.com/MoonshotAI/kimi-code/pull/661) [`0927f79`](https://github.com/MoonshotAI/kimi-code/commit/0927f79883e036d0127d4384f60f8e486afb3b8c) - Cancel active turns during session shutdown so foreground shell commands do not outlive prompt-mode exits. + +- [#604](https://github.com/MoonshotAI/kimi-code/pull/604) [`7ec738c`](https://github.com/MoonshotAI/kimi-code/commit/7ec738c4a1de41b3a042cfb48700dfaf51e9de94) - Fix premature stream close errors when shell processes time out or are killed. + +- [#632](https://github.com/MoonshotAI/kimi-code/pull/632) [`d8cdebf`](https://github.com/MoonshotAI/kimi-code/commit/d8cdebf3c03efa3a3dfa4f1deb3186a8f8f7f5ef) - Degrade unsupported audio/video to placeholder text and reattach tool result media instead of silently dropping them. + +- [#628](https://github.com/MoonshotAI/kimi-code/pull/628) [`0ee9106`](https://github.com/MoonshotAI/kimi-code/commit/0ee91066eaa8ec794c8337faefc14d1b1200ce82) - Fix ACP file reads and edits for Windows workspaces opened through IDE clients. + +- [#658](https://github.com/MoonshotAI/kimi-code/pull/658) [`0381329`](https://github.com/MoonshotAI/kimi-code/commit/0381329570d3dca9fd861761c843968cc1c5e927) - Send OpenAI Responses system prompts as request instructions. + +- [#654](https://github.com/MoonshotAI/kimi-code/pull/654) [`ff80327`](https://github.com/MoonshotAI/kimi-code/commit/ff803273440f3a2ff53d2c529c6fc892fde1d93f) - Propagate configured execution environment overrides across spawned processes. + +- [#644](https://github.com/MoonshotAI/kimi-code/pull/644) [`a58b5b2`](https://github.com/MoonshotAI/kimi-code/commit/a58b5b20bb42228c72277daba9fa07bb1cd539a6) - Polish builtin skills. + +- [#649](https://github.com/MoonshotAI/kimi-code/pull/649) [`a2c5e1b`](https://github.com/MoonshotAI/kimi-code/commit/a2c5e1be25484f7c52f729e333196c485f83b84c) - Add runtime support for dynamic MCP server updates, reference skills, replay timestamps, and Node file uploads. + +- [#631](https://github.com/MoonshotAI/kimi-code/pull/631) [`2961425`](https://github.com/MoonshotAI/kimi-code/commit/296142544ec64e93c9083a51d3a53a83496d10cb) - Find slash commands by their aliases in autocomplete — typing `/clear` now suggests `new (clear)`. + +- [#648](https://github.com/MoonshotAI/kimi-code/pull/648) [`54302ad`](https://github.com/MoonshotAI/kimi-code/commit/54302ad612294056a47ada74b76737f2284861b5) - Prevent overlapping interactive agent requests from using the wrong active agent. + +- [#641](https://github.com/MoonshotAI/kimi-code/pull/641) [`30459af`](https://github.com/MoonshotAI/kimi-code/commit/30459af6abc8308e7f13822d9dbef3a5be80dd4a) - Stop background tasks by default when sessions close. + +- [#645](https://github.com/MoonshotAI/kimi-code/pull/645) [`1b58aa8`](https://github.com/MoonshotAI/kimi-code/commit/1b58aa8cdf675e6f4c02cd083feb55debbe9b3f1) - Add a YOLO choice when starting swarm tasks from Manual mode. + +- [#655](https://github.com/MoonshotAI/kimi-code/pull/655) [`1e2e679`](https://github.com/MoonshotAI/kimi-code/commit/1e2e679693af2fc97826078aa671555a3a900349) - Display a tips banner below the welcome panel on startup. + +## 0.14.0 + +### Minor Changes + +- [#607](https://github.com/MoonshotAI/kimi-code/pull/607) [`b253a82`](https://github.com/MoonshotAI/kimi-code/commit/b253a82a7a5f7d91883dc77a30b8b38f8b6e1470) - Add an `Interrupt` hook event that fires when the user interrupts a turn (e.g. pressing Esc), letting hooks observe the turn stopping instead of getting stuck on a working state. + +### Patch Changes + +- [#626](https://github.com/MoonshotAI/kimi-code/pull/626) [`856ec00`](https://github.com/MoonshotAI/kimi-code/commit/856ec002906f4964086915ceb9aa616b89ab6594) - Preserve image outputs from tools when using OpenAI-compatible chat completions. + +## 0.13.1 + +### Patch Changes + +- [#610](https://github.com/MoonshotAI/kimi-code/pull/610) [`b747c6a`](https://github.com/MoonshotAI/kimi-code/commit/b747c6a9501e208250d09cf9a2810c885c6ce91b) - Add Claude Fable 5 support to the Anthropic provider. + +- [#615](https://github.com/MoonshotAI/kimi-code/pull/615) [`494554e`](https://github.com/MoonshotAI/kimi-code/commit/494554eac5d34d6a3c5c36b6fb2b2e5397b07f0c) - Add an interactive undo selector and clearer undo-limit messages. + +- [#598](https://github.com/MoonshotAI/kimi-code/pull/598) [`32d7080`](https://github.com/MoonshotAI/kimi-code/commit/32d708083730c14090f855b1fcb650e2bc713797) - Clarify active skill prompts so loaded skills are no longer represented as system reminders. + +- [#595](https://github.com/MoonshotAI/kimi-code/pull/595) [`1580f35`](https://github.com/MoonshotAI/kimi-code/commit/1580f35136eed02331dcff6c8482247d5cf35458) - Fix Kimi Datasource to use the matching OAuth credentials and service endpoint for the active Kimi Code environment. + +- [#619](https://github.com/MoonshotAI/kimi-code/pull/619) [`1fbe0e4`](https://github.com/MoonshotAI/kimi-code/commit/1fbe0e4ee89241bee6b5b1d5a4a38b6c6de3c5bf) - Fix goal marker text overflowing terminal width. + +- [#612](https://github.com/MoonshotAI/kimi-code/pull/612) [`4603d8a`](https://github.com/MoonshotAI/kimi-code/commit/4603d8ad6e92a303f396f3d79d4e4d212d1c4b14) - Prevent forking sessions during active turns and consolidate wire protocol definitions into a shared internal package. + +- [#540](https://github.com/MoonshotAI/kimi-code/pull/540) [`2ebe387`](https://github.com/MoonshotAI/kimi-code/commit/2ebe38769fc50215a7c94a362cd4e943130e1143) - Tighten file tool guidance to route incremental edits through Edit. + +- [#606](https://github.com/MoonshotAI/kimi-code/pull/606) [`a1b419a`](https://github.com/MoonshotAI/kimi-code/commit/a1b419ab5901d16ab9527eef62bcd468e76b27a3) - YOLO mode no longer asks before writing or editing files outside the working directory. + +## 0.13.0 + +### Minor Changes + +- [#484](https://github.com/MoonshotAI/kimi-code/pull/484) [`f863127`](https://github.com/MoonshotAI/kimi-code/commit/f863127ab7e8b8e2e9af11c54694c08900e3103a) - Add custom color themes. Define your own palette as a JSON file in `~/.kimi-code/themes/`, or generate one with the built-in `/custom-theme` skill command. + +- [#582](https://github.com/MoonshotAI/kimi-code/pull/582) [`d85dc0b`](https://github.com/MoonshotAI/kimi-code/commit/d85dc0b96a3c98c6951b8f6e6fa8b663d4c95360) - Add `/import-from-cc-codex` to import selected Claude Code and Codex instructions, Skills, and MCP settings. + +- [#593](https://github.com/MoonshotAI/kimi-code/pull/593) [`40506f4`](https://github.com/MoonshotAI/kimi-code/commit/40506f49d689aaf3e920c6bc9ae2b91219ee3f7f) - Show available plugin updates in the marketplace. An installed plugin whose marketplace version is newer than the local version now renders an `update <local> → <latest>` badge (and updates in place on Enter); up-to-date plugins show `installed · v<version>`. The marketplace `version` served in dev and written by the CDN build is now stamped from each plugin's manifest so "latest" stays accurate. + +### Patch Changes + +- [#587](https://github.com/MoonshotAI/kimi-code/pull/587) [`0abde86`](https://github.com/MoonshotAI/kimi-code/commit/0abde8662a531293fc8faa7cf9089c43ad8d6d76) - Clarify grouped subagent progress with active status breakdowns and elapsed time. + +- [#594](https://github.com/MoonshotAI/kimi-code/pull/594) [`f2863af`](https://github.com/MoonshotAI/kimi-code/commit/f2863af267b2e7d5ff5b99ff80c95c379a5b0272) - Fix device login to keep the URL and code visible when the browser cannot be opened. + +- [#591](https://github.com/MoonshotAI/kimi-code/pull/591) [`e48234a`](https://github.com/MoonshotAI/kimi-code/commit/e48234af576e41e630736450c66b690226707bc3) - Fix Windows builds and development launches that could fail when package binaries resolve to command shims. + +- [#586](https://github.com/MoonshotAI/kimi-code/pull/586) [`7cb4a23`](https://github.com/MoonshotAI/kimi-code/commit/7cb4a23e01dfaf0e049891b90a27b36000714151) - Truncate queued message display to a single line with ellipsis when it exceeds terminal width. + +## 0.12.1 + +### Patch Changes + +- [#584](https://github.com/MoonshotAI/kimi-code/pull/584) [`11bb62c`](https://github.com/MoonshotAI/kimi-code/commit/11bb62c12f38d380a0ca1bb89ee2df67f93300e1) - Allow obsolete experimental config entries to remain without blocking startup. + +- [#581](https://github.com/MoonshotAI/kimi-code/pull/581) [`aa3471f`](https://github.com/MoonshotAI/kimi-code/commit/aa3471f5d3d2960834ba3239c0b8459144bc79fa) - Pass through xhigh reasoning effort for OpenAI-compatible chat completions requests. + +## 0.12.0 + +### Minor Changes + +- [#569](https://github.com/MoonshotAI/kimi-code/pull/569) [`d7407b0`](https://github.com/MoonshotAI/kimi-code/commit/d7407b0ecfc87a3840e26ddaddb69e7f52383699) - Enable micro compaction by default while keeping its opt-out flag. + +- [#531](https://github.com/MoonshotAI/kimi-code/pull/531) [`b47734c`](https://github.com/MoonshotAI/kimi-code/commit/b47734ca0bac84e0b2c4ff50cd3d5eedb9e0c7c1) - Detect Homebrew installations and use `brew upgrade kimi-code` for updates instead of falling back to npm. + +- [#487](https://github.com/MoonshotAI/kimi-code/pull/487) [`4d11394`](https://github.com/MoonshotAI/kimi-code/commit/4d113949c8e906c20c7188817926f44786653923) - Honor the standard `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` / `NO_PROXY` environment variables, including SOCKS proxies, for all outbound traffic. + +- [#569](https://github.com/MoonshotAI/kimi-code/pull/569) [`d7407b0`](https://github.com/MoonshotAI/kimi-code/commit/d7407b0ecfc87a3840e26ddaddb69e7f52383699) - Make goals, background questions, and sub-skill discovery available without experimental opt-ins. + +- [#424](https://github.com/MoonshotAI/kimi-code/pull/424) [`72c4b0a`](https://github.com/MoonshotAI/kimi-code/commit/72c4b0adaa6ae0466875cd8e4066c42456195f21) - Add the `/swarm` command for running agent swarms with live progress and rate-limit-aware retries. + +### Patch Changes + +- [#395](https://github.com/MoonshotAI/kimi-code/pull/395) [`879a7ee`](https://github.com/MoonshotAI/kimi-code/commit/879a7eeb33a8bedf18779d74a00d78369dae3db5) - Fix ACP slash skill routing, bootstrap context reads, file and permission edge cases, subagent event handling, and stale-file edit messaging. + +- [#529](https://github.com/MoonshotAI/kimi-code/pull/529) [`3b62b12`](https://github.com/MoonshotAI/kimi-code/commit/3b62b123e68cc4543bfa8fa376c7e8a24fee0afb) - Detect Git Bash installed through Scoop and other Git shims on Windows. + +- [#547](https://github.com/MoonshotAI/kimi-code/pull/547) [`3765a49`](https://github.com/MoonshotAI/kimi-code/commit/3765a491636a57c0f84ba409c325df10f7613a49) - Rework file reference completion in the TUI. + +- [#537](https://github.com/MoonshotAI/kimi-code/pull/537) [`8d0c91f`](https://github.com/MoonshotAI/kimi-code/commit/8d0c91faa1c878e395bffe9bafa89e10736c2384) - Wrap long single-line shell commands in approval prompts so the full command remains visible. + +- [#552](https://github.com/MoonshotAI/kimi-code/pull/552) [`db82e33`](https://github.com/MoonshotAI/kimi-code/commit/db82e33a20fd1ec204672df4ba5bc38800ce8dea) - Fix goal resume behavior by restoring goal state from agent records. + +- [#521](https://github.com/MoonshotAI/kimi-code/pull/521) [`9aba465`](https://github.com/MoonshotAI/kimi-code/commit/9aba465fd8689be998fa8581d04792b3c7c54359) - Fix the `/mcp` status panel border being broken by multi-line MCP server errors, which are now folded onto a single row. + +- [#543](https://github.com/MoonshotAI/kimi-code/pull/543) [`0c3d556`](https://github.com/MoonshotAI/kimi-code/commit/0c3d556778f969b3c99e69e07ecba27af8bd6c29) - Fix session workdir mismatch on Windows caused by inconsistent path separators. + +- [#544](https://github.com/MoonshotAI/kimi-code/pull/544) [`5cff6d6`](https://github.com/MoonshotAI/kimi-code/commit/5cff6d60273a6145ee38539b9c1306adddc66510) - Load Kimi-specific user Skills and global agent instructions from `KIMI_CODE_HOME` when it is set. + +- [#536](https://github.com/MoonshotAI/kimi-code/pull/536) [`b785e26`](https://github.com/MoonshotAI/kimi-code/commit/b785e2698a2da7adc9ef10251a2aed9b243e3b5f) - Show full plan cards directly and remove the Plan card keyboard shortcut. + +- [#555](https://github.com/MoonshotAI/kimi-code/pull/555) [`41ebe9f`](https://github.com/MoonshotAI/kimi-code/commit/41ebe9fb9f403e2ee6a8721640a79faa64e9210a) - Improve goal mode outcome handling with follow-up messages, safer error pauses, and clearer TUI transcript display. + +- [#506](https://github.com/MoonshotAI/kimi-code/pull/506) [`f09ec7b`](https://github.com/MoonshotAI/kimi-code/commit/f09ec7bbb59af42805a93df2993301dbd317ff2d) - Remove the per-turn auto-compaction limit so long conversations can keep compacting instead of failing early. + +- [#473](https://github.com/MoonshotAI/kimi-code/pull/473) [`3787c30`](https://github.com/MoonshotAI/kimi-code/commit/3787c3016a12af3434072da1cb6fd0c95821ea45) - Allow the startup session picker to exit with repeated Ctrl-C or Ctrl-D. + +- [#210](https://github.com/MoonshotAI/kimi-code/pull/210) [`d995928`](https://github.com/MoonshotAI/kimi-code/commit/d995928681fa2446902a0164919cf893b81efd75) - Show the underlying error when migration fails. + +- [#541](https://github.com/MoonshotAI/kimi-code/pull/541) [`2db1bd9`](https://github.com/MoonshotAI/kimi-code/commit/2db1bd9675ef3b6adf3833f05b7b6d87a137c6eb) - Fix thinking text and tool output display for subagents. + +## 0.11.0 + +### Minor Changes + +- [#468](https://github.com/MoonshotAI/kimi-code/pull/468) [`df4f2d6`](https://github.com/MoonshotAI/kimi-code/commit/df4f2d6e8611074cc0b439928f27decba53d2e9a) - Add experimental sub-skill discovery gated by the `KIMI_CODE_EXPERIMENTAL_SUB_SKILL` environment variable. Ships the `sub-skill` builtin bundle (`sub-skill.review`, `sub-skill.consolidate`) for inventorying and consolidating skills into hierarchical groups. + +- [#480](https://github.com/MoonshotAI/kimi-code/pull/480) [`f555c89`](https://github.com/MoonshotAI/kimi-code/commit/f555c89de79c5d7ae59521a9ed360ad1cf045fcd) - Show built-in skills as direct slash commands and group them ahead of external skill commands. + +- [#458](https://github.com/MoonshotAI/kimi-code/pull/458) [`93eb70a`](https://github.com/MoonshotAI/kimi-code/commit/93eb70a727c9724e19a31b0d2fbebb78b7390c78) - Migrate still-relevant environment variables from kimi-cli: + + - `KIMI_MODEL_TEMPERATURE`, `KIMI_MODEL_TOP_P` — sampling parameters applied globally to any `kimi` provider (not tied to `KIMI_MODEL_NAME`). + - `KIMI_MODEL_THINKING_KEEP` — Moonshot preserved-thinking passthrough (`thinking.keep`), injected only while Thinking is on. + - `KIMI_CODE_NO_AUTO_UPDATE` (legacy alias `KIMI_CLI_NO_AUTO_UPDATE`) — fully disables the update preflight (no check, background install, or prompt). + +- [#470](https://github.com/MoonshotAI/kimi-code/pull/470) [`aa610e2`](https://github.com/MoonshotAI/kimi-code/commit/aa610e247deca737101e4de848122db1c8ee9fb3) - Use a fixed 30-minute timeout for subagents and show concise resume instructions when they time out. + +### Patch Changes + +- [#474](https://github.com/MoonshotAI/kimi-code/pull/474) [`658e465`](https://github.com/MoonshotAI/kimi-code/commit/658e4653fc535dad040ac3406d8ccace7a19077e) - Show the upcoming-goal confirmation with the same accent treatment as goal lifecycle messages. + +- [#474](https://github.com/MoonshotAI/kimi-code/pull/474) [`658e465`](https://github.com/MoonshotAI/kimi-code/commit/658e4653fc535dad040ac3406d8ccace7a19077e) - Fix slash command autocomplete so goal text can be submitted when the cursor is before existing text. + +- [#474](https://github.com/MoonshotAI/kimi-code/pull/474) [`658e465`](https://github.com/MoonshotAI/kimi-code/commit/658e4653fc535dad040ac3406d8ccace7a19077e) - Fix queued goals so failed promotion attempts do not lose or duplicate queued work. + +- [#456](https://github.com/MoonshotAI/kimi-code/pull/456) [`3a98713`](https://github.com/MoonshotAI/kimi-code/commit/3a987130500fe5b403b696850165735c7d0ee076) - Show concise provider filtering errors when responses are blocked before visible output. + +- [#442](https://github.com/MoonshotAI/kimi-code/pull/442) [`960a0e2`](https://github.com/MoonshotAI/kimi-code/commit/960a0e2885b5a6a32ccd62506e9dcf4e35206b6f) - Show "unknown command" instead of "too many arguments" when an invalid subcommand is entered. + +- [#474](https://github.com/MoonshotAI/kimi-code/pull/474) [`658e465`](https://github.com/MoonshotAI/kimi-code/commit/658e4653fc535dad040ac3406d8ccace7a19077e) - Fix upcoming-goal queue handling while editing or pasting queued goals. + +- [#457](https://github.com/MoonshotAI/kimi-code/pull/457) [`1fe5d55`](https://github.com/MoonshotAI/kimi-code/commit/1fe5d5549c84de17183c4c76a9713cd8538ca755) - Clamp OpenAI Chat Completions `xhigh` and `max` thinking effort to `high` unless the model supports `xhigh` on `v1/chat/completions`. + +- [#464](https://github.com/MoonshotAI/kimi-code/pull/464) [`4f9977d`](https://github.com/MoonshotAI/kimi-code/commit/4f9977d4dcd2df14e6a310396c37af170b2eac50) - Preserve thinking effort when compacting long conversations. + +- [#474](https://github.com/MoonshotAI/kimi-code/pull/474) [`658e465`](https://github.com/MoonshotAI/kimi-code/commit/658e4653fc535dad040ac3406d8ccace7a19077e) - Ask before starting goals in YOLO mode so users can switch to Auto for unattended work. + +- [#461](https://github.com/MoonshotAI/kimi-code/pull/461) [`2af19e2`](https://github.com/MoonshotAI/kimi-code/commit/2af19e29b9f49163b23cade71d3bcaa6d0b11773) - Refresh provider model metadata when capabilities change without model ID changes. + +- [#474](https://github.com/MoonshotAI/kimi-code/pull/474) [`658e465`](https://github.com/MoonshotAI/kimi-code/commit/658e4653fc535dad040ac3406d8ccace7a19077e) - Start upcoming goals immediately when there is no active goal to wait for. + Support multiline edits when managing upcoming goals. + +- [#474](https://github.com/MoonshotAI/kimi-code/pull/474) [`658e465`](https://github.com/MoonshotAI/kimi-code/commit/658e4653fc535dad040ac3406d8ccace7a19077e) - Highlight goal queue subcommands while typing slash commands. + +## 0.10.1 + +### Patch Changes + +- [#443](https://github.com/MoonshotAI/kimi-code/pull/443) [`15a4c64`](https://github.com/MoonshotAI/kimi-code/commit/15a4c64e5cea45c9f72d8c889f306f1f964a8ac6) - Fix a crash when starting a goal in the TUI. + +## 0.10.0 + +### Minor Changes + +- [#433](https://github.com/MoonshotAI/kimi-code/pull/433) [`85338e9`](https://github.com/MoonshotAI/kimi-code/commit/85338e9f7df5d98234fd42891e9bf2a2e6ad767b) - Add the built-in `update-config` skill — you can now have Kimi edit its own config files. + +- [#420](https://github.com/MoonshotAI/kimi-code/pull/420) [`86a42a2`](https://github.com/MoonshotAI/kimi-code/commit/86a42a26a1e01f1748a937031fa76ebeaa1e28a8) - Add persistent experimental feature toggles and a TUI panel that applies confirmed changes by reloading the current session. + +- [#383](https://github.com/MoonshotAI/kimi-code/pull/383) [`15d71b5`](https://github.com/MoonshotAI/kimi-code/commit/15d71b5130d949c35d9dc2641e807e08d72dce48) - Add /reload to reload the current session and apply updated config files, plus /reload-tui to reload only TUI preferences. + +- [#393](https://github.com/MoonshotAI/kimi-code/pull/393) [`beb12ac`](https://github.com/MoonshotAI/kimi-code/commit/beb12ac0216818a5c5eda24fb304e4ab01792784) - Users now can prepare several goals for the agent to work on sequentially. The agent will pick up the next goal from the queue once the current goal is completed. Use `/goal next <objective>` to queue a goal and `/goal next manage` to review and change the queue interactively. + +- [#431](https://github.com/MoonshotAI/kimi-code/pull/431) [`6a4e4c7`](https://github.com/MoonshotAI/kimi-code/commit/6a4e4c75d4bf6db3fefbb5c115d7a7c324bcae16) - Add a doctor command for validating Kimi Code configuration files. + +### Patch Changes + +- [#393](https://github.com/MoonshotAI/kimi-code/pull/393) [`beb12ac`](https://github.com/MoonshotAI/kimi-code/commit/beb12ac0216818a5c5eda24fb304e4ab01792784) - Stop carrying active and queued goals into forked sessions. + +- [#408](https://github.com/MoonshotAI/kimi-code/pull/408) [`6303bd2`](https://github.com/MoonshotAI/kimi-code/commit/6303bd2936ae168c674af6e685b0eed5a890c42f) - Point session error diagnostics to the `/export-debug-zip` command. + +- [#398](https://github.com/MoonshotAI/kimi-code/pull/398) [`b2801c4`](https://github.com/MoonshotAI/kimi-code/commit/b2801c4dbfe3f7e13f5468bfba1555fa12d1707c) - Set terminal tab titles without renaming the running process. + +- [#403](https://github.com/MoonshotAI/kimi-code/pull/403) [`d645d7e`](https://github.com/MoonshotAI/kimi-code/commit/d645d7e443857b3c974b9fd6065027c0f0cd6953) - Start automatic background updates as soon as startup's fresh update check finds a newer version. + +- [#387](https://github.com/MoonshotAI/kimi-code/pull/387) [`6e74027`](https://github.com/MoonshotAI/kimi-code/commit/6e74027fdc48ad124b2a62465bb5fd07e84d4712) - Lowercase the stale file content message in edit tool errors. + +- [#428](https://github.com/MoonshotAI/kimi-code/pull/428) [`853c5fc`](https://github.com/MoonshotAI/kimi-code/commit/853c5fc43741582ecbde3b4fccf82cddffe3626e) - Ensure Nix-packaged CLI builds can find ripgrep and fd. + +- [#411](https://github.com/MoonshotAI/kimi-code/pull/411) [`4598262`](https://github.com/MoonshotAI/kimi-code/commit/459826292f855592288bcfddaa1c72529a6d8c64) - Normalize malformed Responses stream rate limit errors as provider rate limit failures. + +- [#405](https://github.com/MoonshotAI/kimi-code/pull/405) [`07e2e0f`](https://github.com/MoonshotAI/kimi-code/commit/07e2e0f094fcbc8a6026eb53f5a70cc437bf7c52) - Refresh the update target before showing foreground update prompts so the displayed version matches the install. + +- [#399](https://github.com/MoonshotAI/kimi-code/pull/399) [`232ed87`](https://github.com/MoonshotAI/kimi-code/commit/232ed874d41de777e6ff9c539ac22d830d0b5c3a) - Keep managed OAuth credentials scoped to their configured authentication and API endpoints. + +- [#407](https://github.com/MoonshotAI/kimi-code/pull/407) [`07609b4`](https://github.com/MoonshotAI/kimi-code/commit/07609b41a31499bb5c7811dbab71fa427e621efc) - Set the CLI process title to kimi-code during startup. + +- [#419](https://github.com/MoonshotAI/kimi-code/pull/419) [`d0f8e24`](https://github.com/MoonshotAI/kimi-code/commit/d0f8e24e9b4d2c6dd68d93bc804a4390bf661c10) - Document the Git Bash prerequisite for Windows installs. + +- [#430](https://github.com/MoonshotAI/kimi-code/pull/430) [`be0da5f`](https://github.com/MoonshotAI/kimi-code/commit/be0da5ff39641e117d60045a43a7d5d2e0b85b75) - Fail early when Git Bash is missing on Windows before starting CLI sessions. + +## 0.9.0 + +### Minor Changes + +- [#368](https://github.com/MoonshotAI/kimi-code/pull/368) [`3eafa79`](https://github.com/MoonshotAI/kimi-code/commit/3eafa79f39c06b67d18bd2c1fd5321d2d889ed90) - Add `@moonshot-ai/acp-adapter` and the `kimi acp` subcommand: kimi-code now speaks [Agent Client Protocol 0.23](https://agentclientprotocol.com/) over stdio so IDEs (Zed, JetBrains AI Chat, custom clients) can drive sessions directly — coverage matrix, Zed configuration and breaking pre-release notes are in [kimi acp Subcommand Page](https://moonshotai.github.io/kimi-code/en/reference/kimi-acp.html). + +- [#338](https://github.com/MoonshotAI/kimi-code/pull/338) [`ba7dd73`](https://github.com/MoonshotAI/kimi-code/commit/ba7dd736a3b295b2a29c229a944208c232d51458) - Add `/btw` for side-channel conversations without steering the active main turn. + +- [#357](https://github.com/MoonshotAI/kimi-code/pull/357) [`179aecf`](https://github.com/MoonshotAI/kimi-code/commit/179aecf42379e8ef4091f5351c91cd460ba11bdd) - Log enabled experimental flags at startup. + +- [#378](https://github.com/MoonshotAI/kimi-code/pull/378) [`e0d28b4`](https://github.com/MoonshotAI/kimi-code/commit/e0d28b4941ad6f16e69bdf56a4185655feec5320) - Allow `/btw` to open the side-channel panel before entering a question. + +### Patch Changes + +- [#246](https://github.com/MoonshotAI/kimi-code/pull/246) [`7d1f889`](https://github.com/MoonshotAI/kimi-code/commit/7d1f889d3dc123f44a8d14543e5aaf8aeef2c752) - Fix external editor (Ctrl+G) on Windows by removing `/bin/sh` dependency and using platform-aware shell quoting for temp file paths. + +- [#365](https://github.com/MoonshotAI/kimi-code/pull/365) [`6a22523`](https://github.com/MoonshotAI/kimi-code/commit/6a2252343a0d624b326b2d369ec908bc8d60092d) - Fix goal budget tool schemas for OpenAI-compatible providers. + +- [#365](https://github.com/MoonshotAI/kimi-code/pull/365) [`6a22523`](https://github.com/MoonshotAI/kimi-code/commit/6a2252343a0d624b326b2d369ec908bc8d60092d) - Use the OpenAI completion token field required by newer Chat Completions models. + +- [#380](https://github.com/MoonshotAI/kimi-code/pull/380) [`8639105`](https://github.com/MoonshotAI/kimi-code/commit/86391053139ad4ea437afe79f472412fb1b106a1) - Resume saved subagents lazily when they are accessed. + +- [#339](https://github.com/MoonshotAI/kimi-code/pull/339) [`a6b16ce`](https://github.com/MoonshotAI/kimi-code/commit/a6b16ce6b4bdc20ed33888975c7da7ff1919e22f) - Allow SDK runtime creation to use a separate RPC client while preserving local CLI startup. + +- [#363](https://github.com/MoonshotAI/kimi-code/pull/363) [`90879f3`](https://github.com/MoonshotAI/kimi-code/commit/90879f37af2ddb941223d293a67615f8f557e3af) - Unify the interaction and visuals across TUI dialogs and selectors. + +- [#365](https://github.com/MoonshotAI/kimi-code/pull/365) [`6a22523`](https://github.com/MoonshotAI/kimi-code/commit/6a2252343a0d624b326b2d369ec908bc8d60092d) - Use configured model output limits for completion token caps. + +## 0.8.0 + +### Minor Changes + +- [#319](https://github.com/MoonshotAI/kimi-code/pull/319) [`fe7db4a`](https://github.com/MoonshotAI/kimi-code/commit/fe7db4a7e361b83194eb1ebb52d27daed53be532) - Append the current todo list as markdown to compaction summaries before writing them to history. + +- [#334](https://github.com/MoonshotAI/kimi-code/pull/334) [`eeefa98`](https://github.com/MoonshotAI/kimi-code/commit/eeefa98083e9d037d2ba7c59de9e5eb51b19fdd7) - Add background automatic upgrades, which can be disabled in tui.toml. + +- [#270](https://github.com/MoonshotAI/kimi-code/pull/270) [`ac37d74`](https://github.com/MoonshotAI/kimi-code/commit/ac37d7448458fdb73fbe00e35856dcf44a13f734) - Add experimental goal mode for longer tasks that need more than one turn. Turn it on with `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1` before you start Kimi. + + Use `/goal <objective>` in the TUI when you want Kimi to keep working on one task across turns. For example: + + ```text + /goal Fix the failing checkout test + ``` + + Kimi shows the goal in the TUI and keeps progress visible while it works. Use `/goal status`, `/goal pause`, `/goal resume`, `/goal cancel`, and `/goal replace <objective>` to manage the goal. This feature is still experimental. Try it and tell us what would make it more useful. + +- [#315](https://github.com/MoonshotAI/kimi-code/pull/315) [`191059d`](https://github.com/MoonshotAI/kimi-code/commit/191059d40049d3bfd07661ac03bb961eac1407f7) - Add background structured questions so agents can continue while waiting for user answers. + +- [#313](https://github.com/MoonshotAI/kimi-code/pull/313) [`3c5dee8`](https://github.com/MoonshotAI/kimi-code/commit/3c5dee8836ac823fce01707f60b9c095a963060e) - Add `kimi provider` CLI subcommand with `add`, `remove`, `list`, and `catalog list` / `catalog add` actions, so providers from a custom registry (api.json) or the public models.dev catalog can be imported and managed without launching the TUI. + +- [#277](https://github.com/MoonshotAI/kimi-code/pull/277) [`a217ff0`](https://github.com/MoonshotAI/kimi-code/commit/a217ff09aad0665b1501b156c2cc1f186b876087) - Add `/undo` slash command to withdraw the last prompt from conversation history, and keep replay records in sync when a prompt is undone. + +- [#334](https://github.com/MoonshotAI/kimi-code/pull/334) [`eeefa98`](https://github.com/MoonshotAI/kimi-code/commit/eeefa98083e9d037d2ba7c59de9e5eb51b19fdd7) - Add a `kimi upgrade` command for manually checking and upgrade Kimi Code CLI. + +- [#336](https://github.com/MoonshotAI/kimi-code/pull/336) [`7cda9c3`](https://github.com/MoonshotAI/kimi-code/commit/7cda9c3866bad6b3ce8f95c383a111e1ee5e9325) - Add approval lifecycle hook events for observing pending and completed permission prompts. + +### Patch Changes + +- [#285](https://github.com/MoonshotAI/kimi-code/pull/285) [`573c56e`](https://github.com/MoonshotAI/kimi-code/commit/573c56e829a10e8a45738a37250d8c15f4ab8d8d) - Consolidate background task management under the agent background runtime. + +- [#314](https://github.com/MoonshotAI/kimi-code/pull/314) [`6de3d97`](https://github.com/MoonshotAI/kimi-code/commit/6de3d97d82e2c585035d1d7f969a3504f712df21) - Prevent modified keyboard release sequences from appearing after exiting the CLI. + +- [#335](https://github.com/MoonshotAI/kimi-code/pull/335) [`7284f30`](https://github.com/MoonshotAI/kimi-code/commit/7284f30479142fd66b1e8a731fd00198b1e8684f) - Fix custom registry provider handling during re-import. Prevent loss of multi-provider entries and remove stale providers along with their model aliases and default model references. + +- [#311](https://github.com/MoonshotAI/kimi-code/pull/311) [`80164c2`](https://github.com/MoonshotAI/kimi-code/commit/80164c2e975ba82f7c915dc3fce6cb00b9d29f6e) - Normalize glob patterns before brace expansion to prevent incorrect path matching. + +- [#247](https://github.com/MoonshotAI/kimi-code/pull/247) [`58e2915`](https://github.com/MoonshotAI/kimi-code/commit/58e2915c0f726747a94a8dc5a9eda001ef0d4009) - Fix a crash in the `/sessions` picker on very narrow terminals by clamping every rendered line to the terminal width. + +- [#317](https://github.com/MoonshotAI/kimi-code/pull/317) [`1f8c36a`](https://github.com/MoonshotAI/kimi-code/commit/1f8c36af288ca6120d620f3944c921bc4f0f77ce) - Fix tool output preview rendering: trim trailing empty lines, append ellipsis to multi-line Bash command headers, and truncate long single-line output by visual wrapped lines instead of raw newline count. + +- [#145](https://github.com/MoonshotAI/kimi-code/pull/145) [`d912053`](https://github.com/MoonshotAI/kimi-code/commit/d912053b0d3983f4e67450c347616086cfbd1fe7) - Fix Git Bash path detection on Windows by also searching `usr\bin\bash.exe` locations, which is where bash lives in many Git for Windows installations where `bin\bash.exe` does not exist. + +- [#310](https://github.com/MoonshotAI/kimi-code/pull/310) [`a4511ff`](https://github.com/MoonshotAI/kimi-code/commit/a4511ffc87a1414cb8a5295eeef1103b9ed59645) - Show the full model name in the footer status bar instead of truncating the provider prefix. + +- [#283](https://github.com/MoonshotAI/kimi-code/pull/283) [`91b292e`](https://github.com/MoonshotAI/kimi-code/commit/91b292e898e9d97b0501cf787919d7f1a90c89d8) - Allow glob searches to target explicit absolute paths outside the workspace. + +- [#223](https://github.com/MoonshotAI/kimi-code/pull/223) [`811f252`](https://github.com/MoonshotAI/kimi-code/commit/811f252625bc20a27687b11754b18cc68c7d50dc) - Show MCP server summary in the welcome panel and add configuration hints in the /mcp command output. + +- [#229](https://github.com/MoonshotAI/kimi-code/pull/229) [`fb35bca`](https://github.com/MoonshotAI/kimi-code/commit/fb35bca032486eaefb7b9d7b612d353033e0922c) - Replace chalk named color with theme-aware hex in session-directory warning. + +- [#303](https://github.com/MoonshotAI/kimi-code/pull/303) [`3d7e20e`](https://github.com/MoonshotAI/kimi-code/commit/3d7e20e6978cb35787738e12f6f352fbc2733582) - Point users to `/provider` instead of the removed `/connect` command in the welcome screen and the no-models-configured hint. + +- [#135](https://github.com/MoonshotAI/kimi-code/pull/135) [`0071b63`](https://github.com/MoonshotAI/kimi-code/commit/0071b63fc83821430472e11db3c6aa613c0bdf7e) - Fix slash-activated skills not being recognized by the model due to missing system reminder wrapper. + +- [#330](https://github.com/MoonshotAI/kimi-code/pull/330) [`7a47045`](https://github.com/MoonshotAI/kimi-code/commit/7a47045af2790eba0e68d5406c670ac759b21755) - Allow subagents to use custom tools registered on their parent agent. + +- [#333](https://github.com/MoonshotAI/kimi-code/pull/333) [`1178c5c`](https://github.com/MoonshotAI/kimi-code/commit/1178c5cd148d9d5851574afaafb986be1dfe9b63) - Remind the model to refresh TodoList during long-running tasks and strengthen TodoList progress-tracking guidance. + +- [#327](https://github.com/MoonshotAI/kimi-code/pull/327) [`8809f3e`](https://github.com/MoonshotAI/kimi-code/commit/8809f3eb114172ac64cefe43bbf9b9257c5245c0) - Fix cross-provider replay failures from incompatible tool call IDs and unsigned Claude thinking history. + +## 0.7.0 + +### Minor Changes + +- [#232](https://github.com/MoonshotAI/kimi-code/pull/232) [`a24bfb1`](https://github.com/MoonshotAI/kimi-code/commit/a24bfb1df38e58120827a1d8ed881724af2e7b23) - Add `KIMI_MODEL_ADAPTIVE_THINKING` (and a matching `adaptive_thinking` model-alias field) to force adaptive thinking (`thinking: { type: 'adaptive' }`) on or off, overriding the Anthropic model-name version inference. This lets custom-named compatible endpoints that back an adaptive-capable model opt in even when the model name does not encode a parseable Claude version. + +- [#264](https://github.com/MoonshotAI/kimi-code/pull/264) [`42bb914`](https://github.com/MoonshotAI/kimi-code/commit/42bb9141d8ee7023639f943dd4c6a0f6c8fa8945) - Add `/provider` command for managing AI providers, support custom registry imports, and introduce a tabbed model selector. + +- [#204](https://github.com/MoonshotAI/kimi-code/pull/204) [`ee69d0a`](https://github.com/MoonshotAI/kimi-code/commit/ee69d0ac29f56bde4957c14767d7ca436697d9cf) - Render scheduled reminders distinctly in the TUI, expose cron fired events to SDK clients, and report cron fire times with local timezone offsets. + +### Patch Changes + +- [#282](https://github.com/MoonshotAI/kimi-code/pull/282) [`a580cd3`](https://github.com/MoonshotAI/kimi-code/commit/a580cd3a98664e18642e0e856aeaa9b71ba93516) - Fix glob pattern backslash escaping and include match count in truncation messages. + +- [#260](https://github.com/MoonshotAI/kimi-code/pull/260) [`178827d`](https://github.com/MoonshotAI/kimi-code/commit/178827db47f183df783ba63bf8f1c338f2cbd7e6) - Polish a small TUI visual interaction. + +- [#267](https://github.com/MoonshotAI/kimi-code/pull/267) [`e2e1728`](https://github.com/MoonshotAI/kimi-code/commit/e2e17289fca9bcb23f05cd77f7bcb9cba5db0325) - Report truncated compaction summaries clearly and apply valid completion token budgets across supported providers. + +- [#274](https://github.com/MoonshotAI/kimi-code/pull/274) [`a1dfbfe`](https://github.com/MoonshotAI/kimi-code/commit/a1dfbfeb16bcad0c2c8faa232d6d1ce4a2681d57) - Clarify Kimi Platform API key login labels and prompt details. + +## 0.6.0 + +### Minor Changes + +- [#212](https://github.com/MoonshotAI/kimi-code/pull/212) [`2bbea75`](https://github.com/MoonshotAI/kimi-code/commit/2bbea75ee4c0b11f12d2921061774426df40479a) - Add a `KIMI_MODEL_*` environment-variable channel that lets you run Kimi Code against a specific model (provider type, base URL, API key, context size, capabilities, and thinking settings) without editing `config.toml`. + +- [#221](https://github.com/MoonshotAI/kimi-code/pull/221) [`bab2da7`](https://github.com/MoonshotAI/kimi-code/commit/bab2da7b1c785d6deba25decb1411f8f5a70de8c) - Install plugins directly from GitHub repository URLs, and surface each install's origin and trust level (kimi-official, curated, third-party) in the plugin manager. + +- [#118](https://github.com/MoonshotAI/kimi-code/pull/118) [`8913440`](https://github.com/MoonshotAI/kimi-code/commit/891344054111a05171963cfa524ef749c2855321) - Support querying sessions by sessionId or workDir in listSessions, and show a helpful cd command when resuming a session from a different working directory. + +- [#186](https://github.com/MoonshotAI/kimi-code/pull/186) [`537cf20`](https://github.com/MoonshotAI/kimi-code/commit/537cf20d18b26d4238f963f793f8a8ef085ac97e) - Remove the default per-turn step limit of 1000. Users can still set `max_steps_per_turn` in config to enforce a custom limit. + +### Patch Changes + +- [#197](https://github.com/MoonshotAI/kimi-code/pull/197) [`f3269ea`](https://github.com/MoonshotAI/kimi-code/commit/f3269eacb9da9a6b66f578a864d0b9bdfb1d6d81) - Show the real terminal status of background agents in the transcript so lost, failed, and killed ones no longer appear as completed, and include the resume agent id and recovery instructions in the failure notification so the model can resume reliably. + +- [#211](https://github.com/MoonshotAI/kimi-code/pull/211) [`54590d3`](https://github.com/MoonshotAI/kimi-code/commit/54590d3d464b05eed0837a725b37f3aa491c09af) - Back off failed compaction retries by a fixed slice of the model context window. + +- [#167](https://github.com/MoonshotAI/kimi-code/pull/167) [`b5981a5`](https://github.com/MoonshotAI/kimi-code/commit/b5981a523b66ff2fd5f09a7e66075628b94683c8) - Introduce `ModelProvider` interface and `SingleModelProvider` to decouple `Agent` from `ProviderManager`. + +- [#213](https://github.com/MoonshotAI/kimi-code/pull/213) [`2388f20`](https://github.com/MoonshotAI/kimi-code/commit/2388f20bb3d039e89caefca159801059b90dc64a) - Handle context overflow errors consistently across provider responses. + +- [#214](https://github.com/MoonshotAI/kimi-code/pull/214) [`caaa6d8`](https://github.com/MoonshotAI/kimi-code/commit/caaa6d83ee262ba4c954386458ee13aacdb26e1a) - Fix the native self-updater reporting a successful update when the install command actually failed. + +- [#202](https://github.com/MoonshotAI/kimi-code/pull/202) [`14a0348`](https://github.com/MoonshotAI/kimi-code/commit/14a03488555682dde4bcd74aadf79f60a9827304) - Fix footer leaking onto the terminal when resuming a non-existent session. + +- [#198](https://github.com/MoonshotAI/kimi-code/pull/198) [`8c77cfa`](https://github.com/MoonshotAI/kimi-code/commit/8c77cfab62617e07b38f8514a8ef7cddfd9f1069) - Fix automatic ripgrep installation when temporary files are on another filesystem. + +- [#199](https://github.com/MoonshotAI/kimi-code/pull/199) [`588145d`](https://github.com/MoonshotAI/kimi-code/commit/588145dc9b266456bdb1d739975a5b9cf33d70ae) - Expand the footer's rotating tips to surface more commands and shortcuts, featuring newer and important ones more prominently. + +- [#192](https://github.com/MoonshotAI/kimi-code/pull/192) [`64964a0`](https://github.com/MoonshotAI/kimi-code/commit/64964a0dda98fc2db5e15ba923ea9414c78e0009) - Improve the usage information display in the TUI. + +- [#195](https://github.com/MoonshotAI/kimi-code/pull/195) [`3a0e060`](https://github.com/MoonshotAI/kimi-code/commit/3a0e06031ac6dfde148f64906a06cfe820ad9c63) - Project persisted hook and blocked prompt messages into model context. + +- [#221](https://github.com/MoonshotAI/kimi-code/pull/221) [`bab2da7`](https://github.com/MoonshotAI/kimi-code/commit/bab2da7b1c785d6deba25decb1411f8f5a70de8c) - Restrict plugin trust badges to Kimi-hosted plugin CDN URL patterns. + +- [#207](https://github.com/MoonshotAI/kimi-code/pull/207) [`e280f33`](https://github.com/MoonshotAI/kimi-code/commit/e280f33daf7fbf1271c872dcb224737ec9518f73) - Recover from provider model token limit errors during long conversations. + +- [#201](https://github.com/MoonshotAI/kimi-code/pull/201) [`3da4dae`](https://github.com/MoonshotAI/kimi-code/commit/3da4daeadee39573c7eeede30fa9465b411be3e2) - Automatically retry when a model response stream is dropped mid-flight (a `terminated` error) instead of failing the turn. + +- [#190](https://github.com/MoonshotAI/kimi-code/pull/190) [`1873859`](https://github.com/MoonshotAI/kimi-code/commit/1873859b0ef093a956dfd19e1530e920e7118160) - Slim the LLM diagnostic logs with fewer, more compact fields. + +- [#185](https://github.com/MoonshotAI/kimi-code/pull/185) [`114777e`](https://github.com/MoonshotAI/kimi-code/commit/114777e859680f807375760271533e2dc396af5d) - Split `RuntimeConfig` into `Kaos` and `ToolServices` and update all references accordingly. + +- [#189](https://github.com/MoonshotAI/kimi-code/pull/189) [`564721f`](https://github.com/MoonshotAI/kimi-code/commit/564721fe16e582b2774835b01dec799cbb1d0122) - Clarify subagent and background task stop messages as user-initiated. + +- [#206](https://github.com/MoonshotAI/kimi-code/pull/206) [`07d51e4`](https://github.com/MoonshotAI/kimi-code/commit/07d51e4add6ee23a56fb8745aa7754f05f3d6d36) - Relocate shared tool service typing to the tool support layer. + +- [#215](https://github.com/MoonshotAI/kimi-code/pull/215) [`b9860e9`](https://github.com/MoonshotAI/kimi-code/commit/b9860e9f6ec65eb5dfdabbad54f1a87d69f4f00a) - Align the datasource plugin with the generic two-tool workflow. + +- [#200](https://github.com/MoonshotAI/kimi-code/pull/200) [`5159af3`](https://github.com/MoonshotAI/kimi-code/commit/5159af341c7d388a158e41afb470a2281333f329) - Keep blocked prompt hook conversations available to subsequent model turns. + +## 0.5.0 + +### Minor Changes + +- [#163](https://github.com/MoonshotAI/kimi-code/pull/163) [`07dd604`](https://github.com/MoonshotAI/kimi-code/commit/07dd604c3c7f453dfb0c0a601bb1c44a8114bb3b) - Add `/auto` slash command and `--auto` CLI flag for auto permission mode. + +- [#157](https://github.com/MoonshotAI/kimi-code/pull/157) [`971fce6`](https://github.com/MoonshotAI/kimi-code/commit/971fce6e528c2b210df1852d7cd12bcda71014fd) - Add scheduled tasks: + + You can now ask the agent to remind you at a specific time, run a task on a recurring cron schedule (for example, check a deploy every 5 minutes or run a daily report every weekday at 9am), or come back on its own in a few minutes to continue what it was doing. + + Schedules use the standard 5-field cron syntax. + +### Patch Changes + +- [#162](https://github.com/MoonshotAI/kimi-code/pull/162) [`f3c1015`](https://github.com/MoonshotAI/kimi-code/commit/f3c1015b677d40fb94957ab121da5e14480a890f) - Add a clickable changelog link to the update prompt. + +- [#150](https://github.com/MoonshotAI/kimi-code/pull/150) [`8b5a251`](https://github.com/MoonshotAI/kimi-code/commit/8b5a25161ceac02894d1a09c78a5aa883e460c8e) - Show the full Bash command when expanding a Bash tool card with `ctrl+o`. The header still truncates long commands at 60 chars, but the expanded view now reveals the complete multi-line command above the output. + +- [#158](https://github.com/MoonshotAI/kimi-code/pull/158) [`d1f9a83`](https://github.com/MoonshotAI/kimi-code/commit/d1f9a83d7af16ab78b7da571b3de146767864f3a) - Shorten the session title written to the terminal window/tab from 80 to 32 characters so long first messages and pasted content no longer stretch the tab bar past readable width. + +- [#146](https://github.com/MoonshotAI/kimi-code/pull/146) [`76cbf86`](https://github.com/MoonshotAI/kimi-code/commit/76cbf86e2035f905242d30009052254eee52bcf8) - Cap the inline todo panel at five rows and show a `+N more` indicator so long task lists no longer fill the screen. + +- [#120](https://github.com/MoonshotAI/kimi-code/pull/120) [`8515472`](https://github.com/MoonshotAI/kimi-code/commit/85154724764a3478bfc0ef40d8b5a1def5063ec7) - Fix compaction to handle edge cases where no messages are compactable and improve retry logic. + +- [#159](https://github.com/MoonshotAI/kimi-code/pull/159) [`c88b7bf`](https://github.com/MoonshotAI/kimi-code/commit/c88b7bf0efcf6f0e5f904c20471ab865cb912e40) - Fix official datasource tools to preserve complete responses and write returned result files. + +- [#124](https://github.com/MoonshotAI/kimi-code/pull/124) [`3e72f25`](https://github.com/MoonshotAI/kimi-code/commit/3e72f25ad93dac02456ebb1e29d80cf904258c14) - Fix migration mapping the legacy `default_yolo` key to the dead `yolo` field instead of `default_permission_mode`. + +- [#164](https://github.com/MoonshotAI/kimi-code/pull/164) [`0a76658`](https://github.com/MoonshotAI/kimi-code/commit/0a766584cba68b2e906a5528c286a8481bd47ed3) - Clarify plugin manager keyboard shortcuts and show plugin state changes inline. + +- [#142](https://github.com/MoonshotAI/kimi-code/pull/142) [`dad2b87`](https://github.com/MoonshotAI/kimi-code/commit/dad2b87ceeb054204027709751f72baadf04b708) - Refactor TUI code structure. + +- [#166](https://github.com/MoonshotAI/kimi-code/pull/166) [`92e1d8c`](https://github.com/MoonshotAI/kimi-code/commit/92e1d8c72bfb1ab31a46608120670698bbf582b8) - Report discovered plugin skills in plugin manager summaries. + +- [#139](https://github.com/MoonshotAI/kimi-code/pull/139) [`50251a1`](https://github.com/MoonshotAI/kimi-code/commit/50251a136093c27c0d69a730b267b746dea47468) - Show file content and diff in Write and Edit approval prompts, and open them in a dedicated full-screen viewer on ctrl+e instead of expanding inline. + +- [#117](https://github.com/MoonshotAI/kimi-code/pull/117) [`a6d379b`](https://github.com/MoonshotAI/kimi-code/commit/a6d379b2ceea4bf988517bdf357d1931a1fb1f05) - Offload large base64 media payloads from wire.jsonl into external blob files to reduce wire size and memory pressure during session replay. Includes an in-memory read-through cache on `BlobStore` so repeated rehydration avoids redundant disk reads. + +- [#150](https://github.com/MoonshotAI/kimi-code/pull/150) [`8b5a251`](https://github.com/MoonshotAI/kimi-code/commit/8b5a25161ceac02894d1a09c78a5aa883e460c8e) - Wrap long question, body, and option text in the AskUserQuestion dialog instead of truncating with an ellipsis. The question prompt, body description, option label, option description, and submit-tab review entries now flow onto multiple lines with a hanging indent. + +## 0.4.0 + +### Minor Changes + +- [#116](https://github.com/MoonshotAI/kimi-code/pull/116) [`2c7a8cc`](https://github.com/MoonshotAI/kimi-code/commit/2c7a8cc010a7b8134c5f16185e031a6de4585165) - Expand folded paste markers on second paste. When the cursor is on a paste marker (e.g. `[paste [#1](https://github.com/MoonshotAI/kimi-code/issues/1) +15 lines]`) and the user pastes again, the marker expands back to the original content instead of inserting new clipboard data. + +- [#26](https://github.com/MoonshotAI/kimi-code/pull/26) [`2b74025`](https://github.com/MoonshotAI/kimi-code/commit/2b74025302be9b42e68a15f33333c55d64a6c9e7) - Rework tool permissions: reads outside cwd no longer prompt, session approvals match the exact call, and path-based rules are case-insensitive. + +- [#119](https://github.com/MoonshotAI/kimi-code/pull/119) [`ebf6e81`](https://github.com/MoonshotAI/kimi-code/commit/ebf6e8181ea20a0fcf6a609195ccf5b6cc2a665a) - Add user-global plugin installation, interactive plugin management, plugin-provided skills, and plugin-owned MCP servers. + +- [#112](https://github.com/MoonshotAI/kimi-code/pull/112) [`d03f6f4`](https://github.com/MoonshotAI/kimi-code/commit/d03f6f4fa582314a4330d0049fed6a0baae7271a) - Add `/export-debug-zip` slash command to export the current session as a debug ZIP archive directly from the TUI. + +- [#113](https://github.com/MoonshotAI/kimi-code/pull/113) [`028d069`](https://github.com/MoonshotAI/kimi-code/commit/028d069b12d8377c5c307b94f11f02233d9c0a26) - Add `/export-md` slash command to export the current session as a Markdown file. + +### Patch Changes + +- [#105](https://github.com/MoonshotAI/kimi-code/pull/105) [`d599183`](https://github.com/MoonshotAI/kimi-code/commit/d599183c8eccea813d7aa5ddd974e72139cbb63c) - Enhance `kimi export` to include more diagnostic information in the manifest. + +- [#89](https://github.com/MoonshotAI/kimi-code/pull/89) [`61cae59`](https://github.com/MoonshotAI/kimi-code/commit/61cae592fac0f1d824ee28263375937452f719ff) - Prevent the TUI from crashing when pull request lookup fails during startup. + +- [#97](https://github.com/MoonshotAI/kimi-code/pull/97) [`2e8c417`](https://github.com/MoonshotAI/kimi-code/commit/2e8c417818bb68a71789e4966f18c2be6d39d835) - Fix thinking spinner leaking past turn end when an empty thinking delta creates an orphaned thinking component. + +- [#103](https://github.com/MoonshotAI/kimi-code/pull/103) [`73c4232`](https://github.com/MoonshotAI/kimi-code/commit/73c4232e711c8e7c701d21a07c7b6aace3476360) - Show the original session resume command after forking a session. + +- [#88](https://github.com/MoonshotAI/kimi-code/pull/88) [`ce420bf`](https://github.com/MoonshotAI/kimi-code/commit/ce420bf1c6825080d4c7ec9e155f96039d3376e7) - Refactor TUI resume replay logic. + +- [#119](https://github.com/MoonshotAI/kimi-code/pull/119) [`ebf6e81`](https://github.com/MoonshotAI/kimi-code/commit/ebf6e8181ea20a0fcf6a609195ccf5b6cc2a665a) - Restrict plugin zip installs to manifests at the archive root or a single wrapper directory. + +- [#102](https://github.com/MoonshotAI/kimi-code/pull/102) [`6f55f1d`](https://github.com/MoonshotAI/kimi-code/commit/6f55f1d0aff12ce13cea616a1f37e6242beb2ff8) - Route session-tagged log entries exclusively to the session sink instead of duplicating them to the global sink. Consistently omit stable main-agent context keys from all session log lines that carry `agentId=main`. + +- [#92](https://github.com/MoonshotAI/kimi-code/pull/92) [`4e458d6`](https://github.com/MoonshotAI/kimi-code/commit/4e458d63643a56a2fb1ba9f908c774e56eef1c75) - Use one retry classification for transient LLM failures across regular turns and compaction. + +## 0.3.0 + +### Minor Changes + +- [#76](https://github.com/MoonshotAI/kimi-code/pull/76) [`6f22ae4`](https://github.com/MoonshotAI/kimi-code/commit/6f22ae48f84a062a65dcaa9510ffe96f40ab503b) - /logout now opens a picker so you can choose which provider to log out of, instead of always logging out the one tied to the current model. The current provider is highlighted by default, so pressing Enter matches the previous behavior. The command is also available as /disconnect. + +### Patch Changes + +- [#62](https://github.com/MoonshotAI/kimi-code/pull/62) [`e2b2b46`](https://github.com/MoonshotAI/kimi-code/commit/e2b2b46fc9c1d6a0ada67c590b8aa56e77c9c513) - Make `AgentRecords` hold the `Agent` instance directly and inline the restore dispatch logic. + +- [#73](https://github.com/MoonshotAI/kimi-code/pull/73) [`bddc60f`](https://github.com/MoonshotAI/kimi-code/commit/bddc60f0e9af44d326dc0759a60bce93187f8a7b) - Prevent running the `/model` and `/sessions` slash commands while streaming or compacting context. + +- [#70](https://github.com/MoonshotAI/kimi-code/pull/70) [`d95b013`](https://github.com/MoonshotAI/kimi-code/commit/d95b01342a7921f0863ceb37abad7984d0245509) - Preserve catalog-declared interleaved reasoning fields for OpenAI-compatible models configured through `/connect`. + +- [#78](https://github.com/MoonshotAI/kimi-code/pull/78) [`61f7d0e`](https://github.com/MoonshotAI/kimi-code/commit/61f7d0e7a2b9933bdbe7eef9177e67e7386154a2) - Make OpenAI-compatible reasoner models work out of the box for hand-written provider configs. The `openai` provider now auto-detects thinking on incoming responses by scanning the de facto field set (`reasoning_content`, `reasoning_details`, `reasoning`), serializes thinking back as `reasoning_content` by default, and auto-injects `reasoning_effort` whenever the conversation history contains prior thinking — so DeepSeek, Qwen, One API and other gateway-fronted services no longer require a hand-set `reasoning_key`. The `reasoning_key` model-alias field remains available as an explicit override for non-standard gateways. + +- [#66](https://github.com/MoonshotAI/kimi-code/pull/66) [`8ddfc04`](https://github.com/MoonshotAI/kimi-code/commit/8ddfc0433e3a3a51f326116607d28b0f409e7d93) - Fix API key input dialog showing a masked dot in empty state. + +- [#72](https://github.com/MoonshotAI/kimi-code/pull/72) [`0ce0072`](https://github.com/MoonshotAI/kimi-code/commit/0ce0072cb44ea2bd3a7ca9c54d141c150f0bbb77) - Fix user skills in ~/.agents/ not being loaded. + +- [#86](https://github.com/MoonshotAI/kimi-code/pull/86) [`5e354d0`](https://github.com/MoonshotAI/kimi-code/commit/5e354d0cc89816228d08c3ded17e75201fb300de) - Restore real-time token display for running subagents in the TUI. + +- [#57](https://github.com/MoonshotAI/kimi-code/pull/57) [`8fb61f9`](https://github.com/MoonshotAI/kimi-code/commit/8fb61f9a3ead02bbd79f3a5ab605aba26e1cb847) - Hide the todo panel on resume when all todos are already completed. + +- [#83](https://github.com/MoonshotAI/kimi-code/pull/83) [`7d9216d`](https://github.com/MoonshotAI/kimi-code/commit/7d9216d5aa1e96734c46c8d5d810ec7ed27b2275) - Always emit a paired tool result when a tool returns a malformed or missing result, preventing the next request from failing with a missing tool_call_id error. + +- [#81](https://github.com/MoonshotAI/kimi-code/pull/81) [`1fbefc9`](https://github.com/MoonshotAI/kimi-code/commit/1fbefc99398d4a8ebebb377ff7ca2846483d1a9a) - Improve the Write tool UX. + +- [#79](https://github.com/MoonshotAI/kimi-code/pull/79) [`5a90b53`](https://github.com/MoonshotAI/kimi-code/commit/5a90b53b045099ecb582a36d546e90a3978f0a75) - Fix Plan mode session resets so new sessions no longer fail after plan review rejection and continue receiving events after setup errors. + +- [#77](https://github.com/MoonshotAI/kimi-code/pull/77) [`fe60c21`](https://github.com/MoonshotAI/kimi-code/commit/fe60c215be8979f6abc8258e5255c66dd73d5a19) - Exit promptly when the controlling terminal goes away. The TUI now handles `SIGHUP` / `SIGTERM` and stdout/stderr `EIO` / `EPIPE` / `ENOTCONN` errors, preventing leftover `kimi` processes that pin a CPU core after the parent shell or multiplexer dies unexpectedly. + +- [#85](https://github.com/MoonshotAI/kimi-code/pull/85) [`2bb50a3`](https://github.com/MoonshotAI/kimi-code/commit/2bb50a38d8379e2fac57547b1a563722f713c8fd) - Avoid overly small local completion caps that can truncate reasoning before summaries are produced. + +## 0.2.0 + +### Minor Changes + +- [#30](https://github.com/MoonshotAI/kimi-code/pull/30) [`a200a29`](https://github.com/MoonshotAI/kimi-code/commit/a200a297ac8986ec4baa8d2cdc881ef71bc3abfc) - Add a `/connect` command that configures a provider and model from a model catalog. + +- [#30](https://github.com/MoonshotAI/kimi-code/pull/30) [`a200a29`](https://github.com/MoonshotAI/kimi-code/commit/a200a297ac8986ec4baa8d2cdc881ef71bc3abfc) - The `/connect` provider and model pickers now support type-to-search filtering, and long lists are paginated. The `/model` picker is also paginated when many models are configured. + +- [#25](https://github.com/MoonshotAI/kimi-code/pull/25) [`c4dd1c7`](https://github.com/MoonshotAI/kimi-code/commit/c4dd1c7ff298290ee17d4a6676f93284621f32e8) - Flatten tool call data by inlining tool names and arguments at the top level, and limit legacy record migration so it only rewrites matching tool call payloads. + +### Patch Changes + +- [#9](https://github.com/MoonshotAI/kimi-code/pull/9) [`e503e69`](https://github.com/MoonshotAI/kimi-code/commit/e503e6963ab6cc6b4ed98c89389dbbb525fc6e9e) - Add `Ctrl-J` as an additional shortcut for inserting new lines in the TUI prompt. + +- [#22](https://github.com/MoonshotAI/kimi-code/pull/22) [`2004aed`](https://github.com/MoonshotAI/kimi-code/commit/2004aedfe1d4e5e17762108bf48b7b9aa6d4e25b) - Add wire record migration handling during session replay. + +- [#33](https://github.com/MoonshotAI/kimi-code/pull/33) [`ab4bd09`](https://github.com/MoonshotAI/kimi-code/commit/ab4bd090825cffbd7ab656b47840b0060d6cf601) - Report the macOS product version in OAuth device information instead of the Darwin kernel version. + +- [#52](https://github.com/MoonshotAI/kimi-code/pull/52) [`064343a`](https://github.com/MoonshotAI/kimi-code/commit/064343a6e565a525fbf38b3a1f70f7ff0235a5ed) - Correct the `X-Msh-Platform` header value to `kimi_code_cli`. + +- [#38](https://github.com/MoonshotAI/kimi-code/pull/38) [`e9e4a48`](https://github.com/MoonshotAI/kimi-code/commit/e9e4a48633f2d216672e8905b0235107b5cbe34a) - Clarify the prompt-mode error when no model is configured by pointing users to the login flow. + +- [#13](https://github.com/MoonshotAI/kimi-code/pull/13) [`35726d7`](https://github.com/MoonshotAI/kimi-code/commit/35726d7a41d54a0e6cb19a21d16980fd462132e1) - Hide the empty current session from the sessions picker while keeping other empty sessions visible. + +- [#31](https://github.com/MoonshotAI/kimi-code/pull/31) [`475ebad`](https://github.com/MoonshotAI/kimi-code/commit/475ebadc2070e3b878789f6a89ce191b1bd957a9) - Stop mentioning OAuth credentials in the migration UI — they are never migrated, so the previous "needs /login" notice misread as a failure. OAuth-only installs no longer trigger the migration screen. + +- [#31](https://github.com/MoonshotAI/kimi-code/pull/31) [`475ebad`](https://github.com/MoonshotAI/kimi-code/commit/475ebadc2070e3b878789f6a89ce191b1bd957a9) - Migrate user skills from `~/.kimi/skills/` to `~/.kimi-code/skills/` during the first-launch migration; existing target skills are kept. + +- [#30](https://github.com/MoonshotAI/kimi-code/pull/30) [`a200a29`](https://github.com/MoonshotAI/kimi-code/commit/a200a297ac8986ec4baa8d2cdc881ef71bc3abfc) - When no models are configured, `/model` and the welcome panel now point users to `/login` (for Kimi) and `/connect` (for other providers). + +- [#11](https://github.com/MoonshotAI/kimi-code/pull/11) [`15b018f`](https://github.com/MoonshotAI/kimi-code/commit/15b018fc84a36a9ebde598970e5b44bebe5d68c6) - Surface API-provided error messages during feedback, usage, login, and model setup failures. + +- [#24](https://github.com/MoonshotAI/kimi-code/pull/24) [`7858821`](https://github.com/MoonshotAI/kimi-code/commit/7858821f2f1fecc9de666780fc62434ca76dcc82) - Persist model selections from the terminal UI to the default configuration, and honor the configured default thinking state for new sessions. + +- [#14](https://github.com/MoonshotAI/kimi-code/pull/14) [`0da6073`](https://github.com/MoonshotAI/kimi-code/commit/0da60730b9716c39a07e8a3a0a320e3af7ad30fa) - Move wire metadata handling into the record layer and keep persistence backends limited to storage operations. + +- [#12](https://github.com/MoonshotAI/kimi-code/pull/12) [`89ea895`](https://github.com/MoonshotAI/kimi-code/commit/89ea8959eb9419d04e63645b4d89ca0e33f20d98) - Retry compaction responses that do not contain a summary before updating conversation history. + +- [#29](https://github.com/MoonshotAI/kimi-code/pull/29) [`df7a9ca`](https://github.com/MoonshotAI/kimi-code/commit/df7a9cab606e0f152bc45b1d1645d76210b1e0c4) - Avoid CPU spikes from large streamed tool arguments and coalesce high-frequency streaming UI updates. + +- [#47](https://github.com/MoonshotAI/kimi-code/pull/47) [`07ed2cf`](https://github.com/MoonshotAI/kimi-code/commit/07ed2cf9d4f01985c00c004b3bc0cc8d2587044b) - Emit session resume hint as a structured meta message in stream-json output format. + +- [#49](https://github.com/MoonshotAI/kimi-code/pull/49) [`cf2227e`](https://github.com/MoonshotAI/kimi-code/commit/cf2227e8a5222ad9bd1167b573b62599d0efd906) - Resume sessions with a newer wire protocol version instead of failing. A warning is now shown in the TUI and records are replayed without migration. + +- [#18](https://github.com/MoonshotAI/kimi-code/pull/18) [`a964bd2`](https://github.com/MoonshotAI/kimi-code/commit/a964bd2430a583ff0364fde19eafabda03b489ed) - Warn tmux users when extended key settings may prevent modified Enter shortcuts from working. + +- [#17](https://github.com/MoonshotAI/kimi-code/pull/17) [`bfbd522`](https://github.com/MoonshotAI/kimi-code/commit/bfbd522a7160e597d673550f09fd4af089bfde34) - Let Kimi requests use the remaining context window for completion tokens by default while keeping explicit environment limits as hard caps. diff --git a/apps/kimi-code/README.md b/apps/kimi-code/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7c92bc3a53adecb7578d40d04ae0bf86fb279a7e --- /dev/null +++ b/apps/kimi-code/README.md @@ -0,0 +1,90 @@ +# @moonshot-ai/kimi-code + +> The Starting Point for Next-Gen Agents + +[![npm](https://img.shields.io/npm/v/@moonshot-ai/kimi-code)](https://www.npmjs.com/package/@moonshot-ai/kimi-code) [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![Docs](https://img.shields.io/badge/docs-online-blue)](https://moonshotai.github.io/kimi-code/en/) + +## What is Kimi Code CLI + +Kimi Code CLI is an AI coding agent that runs in your terminal. It can read and edit code, run shell commands, search files, fetch web pages, and choose the next step based on the feedback it receives. It works out of the box with Moonshot AI's Kimi models and can also be configured to use other compatible providers. + +## Install + +The recommended install path is the official script. It does not require Node.js to be installed first. + +- **macOS / Linux**: + +```sh +curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +``` + +- **Windows (PowerShell)**: + +```powershell +irm https://code.kimi.com/kimi-code/install.ps1 | iex +``` + +> On Windows, install [Git for Windows](https://gitforwindows.org/) before first launch because Kimi Code CLI uses the bundled Git Bash as its shell environment. If Git Bash is installed in a custom location, set `KIMI_SHELL_PATH` to the absolute path of `bash.exe`. + +Then run it with a new Terminal session: + +```sh +kimi --version +``` + +### Alternative: npm + +If you prefer npm, use Node.js 22.19.0 or later: + +```sh +npm install -g @moonshot-ai/kimi-code +``` + +Or with pnpm: + +```sh +pnpm add -g @moonshot-ai/kimi-code +``` + +For upgrade and uninstall instructions, see the [Getting Started guide](https://moonshotai.github.io/kimi-code/en/guides/getting-started). + +## Quick Start + +Open a project and start the interactive UI: + +```sh +cd your-project +kimi +``` + +On first launch, run `/login` inside Kimi Code CLI and choose either Kimi Code OAuth or a Kimi Platform API key. After login, try a first task: + +``` +Take a look at this project and explain the main directories. +``` + +## Key Features + +- **Single-binary distribution.** Install with one command — no Node.js setup, no PATH gymnastics, no global module conflicts. +- **Blazing-fast startup.** The TUI is ready in milliseconds, so opening a session never feels heavy. +- **Polished TUI.** A carefully tuned interface designed for long, focused agent sessions. +- **Video input.** Drop a screen recording or demo clip into the chat — let the agent watch instead of typing out what's hard to describe in words. +- **AI-native MCP configuration.** Add, edit, and authenticate Model Context Protocol servers conversationally via `/mcp-config` — no hand-editing JSON. +- **Subagents for focused, parallel work.** Dispatch built-in `coder`, `explore`, and `plan` subagents in isolated context windows; the main conversation stays clean. +- **Lifecycle hooks.** Run local commands at key points — gate risky tool calls, audit decisions, fire desktop notifications, wire into your own automation. + +## Documentation + +- Full docs: https://moonshotai.github.io/kimi-code/en/ +- 中文文档: https://moonshotai.github.io/kimi-code/zh/ +- Getting Started: https://moonshotai.github.io/kimi-code/en/guides/getting-started + +## Repository & Issues + +- Source: https://github.com/MoonshotAI/kimi-code +- Issues: https://github.com/MoonshotAI/kimi-code/issues +- Security: see SECURITY.md in the main repository + +## License + +MIT diff --git a/apps/kimi-code/package.json b/apps/kimi-code/package.json new file mode 100644 index 0000000000000000000000000000000000000000..9fa9267930c2a090bda1261fd3443df5d2a13e6d --- /dev/null +++ b/apps/kimi-code/package.json @@ -0,0 +1,120 @@ +{ + "name": "@moonshot-ai/kimi-code", + "version": "0.43.1", + "description": "The Starting Point for Next-Gen Agents", + "license": "MIT", + "author": "Moonshot AI", + "homepage": "https://github.com/MoonshotAI/kimi-code/tree/main/apps/kimi-code#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/MoonshotAI/kimi-code.git", + "directory": "apps/kimi-code" + }, + "bugs": { + "url": "https://github.com/MoonshotAI/kimi-code/issues" + }, + "keywords": [ + "kimi", + "kimi-code", + "cli", + "agent", + "coding-agent", + "ai", + "tui" + ], + "bin": { + "kimi": "dist/main.mjs" + }, + "files": [ + "dist", + "dist-web", + "native", + "scripts/postinstall.mjs", + "scripts/postinstall", + "README.md" + ], + "type": "module", + "imports": { + "#/tui/theme": "./src/tui/theme/index.ts", + "#/tui/commands": "./src/tui/commands/index.ts", + "#/cli/sub/web": "./src/cli/sub/web/index.ts", + "#/cli/sub/web/*": "./src/cli/sub/web/*.ts", + "#/generated/vis-web-asset": [ + "./src/generated/vis-web-asset.ts", + "./src/generated/vis-web-asset.d.ts" + ], + "#/*": "./src/*.ts" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "scripts": { + "build": "tsdown && tsdown --config tsdown.dist-worker.config.ts && node scripts/copy-native-assets.mjs && node scripts/check-web-assets.mjs", + "prebuild": "node scripts/build-vis-asset.mjs", + "catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json", + "smoke": "node scripts/smoke.mjs", + "build:native:js": "node scripts/native/01-bundle.mjs", + "build:native:sea": "node scripts/native/build.mjs --profile=local", + "build:native:release": "node scripts/native/build.mjs --profile=release", + "package:native": "node scripts/native/package.mjs", + "produce:native:manifest": "node scripts/native/produce-manifest.mjs", + "release:native:resolve": "node scripts/native/resolve-release.mjs", + "test:native:smoke": "node scripts/native/smoke.mjs", + "dev": "node scripts/dev.mjs", + "dev:cli-only": "tsx --import ../../build/register-raw-text-loader.mjs ./src/main.ts", + "dev:server": "KIMI_CODE_DEV_SERVER=1 tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints", + "dev:kap-server": "KIMI_CODE_DEV_SERVER=1 tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints", + "dev:kap-server:multi": "KIMI_CODE_DEV_SERVER=1 tsx --tsconfig ./tsconfig.dev.json --import ../../build/register-raw-text-loader.mjs ./src/main.ts web --no-open --debug-endpoints", + "dev:server:restart": "node scripts/dev-server-restart.mjs", + "dev:plugin-marketplace": "node scripts/dev-plugin-marketplace-server.mjs", + "build:plugin-marketplace": "node scripts/build-plugin-marketplace-cdn.mjs", + "dev:prod": "node dist/main.mjs", + "clean": "rm -rf dist", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "pnpm -w run build:packages && vitest run", + "e2e": "pnpm -w run build:packages && KIMI_E2E=1 vitest run test/e2e", + "e2e:real": "pnpm -w run build:packages && KIMI_E2E_REAL=1 vitest run test/e2e/real-llm-smoke.e2e.test.ts", + "postinstall": "node scripts/postinstall.mjs" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "^0.3.9", + "node-pty": "^1.1.0" + }, + "devDependencies": { + "@moonshot-ai/acp-server": "workspace:^", + "@moonshot-ai/agent-core-v2": "workspace:^", + "@moonshot-ai/kap-server": "workspace:^", + "@moonshot-ai/kimi-code-oauth": "workspace:^", + "@moonshot-ai/kimi-code-sdk": "workspace:^", + "@moonshot-ai/kimi-telemetry": "workspace:^", + "@moonshot-ai/migration-legacy": "workspace:^", + "@moonshot-ai/minidb": "workspace:^", + "@moonshot-ai/pi-tui": "workspace:^", + "@moonshot-ai/remote-control": "workspace:^", + "@moonshot-ai/vis-server": "workspace:^", + "@moonshot-ai/vis-web": "workspace:*", + "@types/qrcode": "^1.5.6", + "@types/semver": "^7.7.0", + "@types/ws": "^8.18.0", + "@types/yazl": "^2.4.6", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "commander": "^13.1.0", + "jimp": "^1.6.1", + "pathe": "^2.0.3", + "postject": "1.0.0-alpha.6", + "semver": "^7.7.4", + "smol-toml": "^1.6.1", + "tsx": "^4.21.0", + "yazl": "^3.3.1", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=22.19.0" + }, + "dependencies": { + "qrcode": "^1.5.4", + "ws": "^8.18.0" + } +} diff --git a/apps/kimi-code/tsconfig.dev.json b/apps/kimi-code/tsconfig.dev.json new file mode 100644 index 0000000000000000000000000000000000000000..4157e7ee969533537754468152f422d1953e364a --- /dev/null +++ b/apps/kimi-code/tsconfig.dev.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "experimentalDecorators": true + }, + "include": [ + "src", + "test", + "../../packages/*/src/**/*.ts", + "../../packages/*/src/**/*.tsx", + "../../packages/*/test/**/*.ts" + ] +} diff --git a/apps/kimi-code/tsconfig.json b/apps/kimi-code/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..a7552cb620057ff54fb76eb65dc79e389a3e9680 --- /dev/null +++ b/apps/kimi-code/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "allowJs": true, + "experimentalDecorators": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "test", "../../packages/agent-core-v2/src/env.d.ts"] +} diff --git a/apps/kimi-code/tsdown.config.ts b/apps/kimi-code/tsdown.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..858aeeb48ceed3b8fe6dbe6ba8cd8c73ba7eb3fe --- /dev/null +++ b/apps/kimi-code/tsdown.config.ts @@ -0,0 +1,40 @@ +import { resolve } from 'node:path'; + +import { defineConfig } from 'tsdown'; + +import { rawTextPlugin } from '../../build/raw-text-plugin.mjs'; +import { BUILT_IN_CATALOG_DEFINE, builtInCatalogDefine } from './scripts/built-in-catalog.mjs'; + +const appRoot = import.meta.dirname; + +export default defineConfig({ + entry: ['./src/main.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, + dts: false, + hash: false, + banner: { + js: [ + '#!/usr/bin/env node', + "import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';", + "import { dirname as __cjsShimDirname } from 'node:path';", + 'const __filename = __cjsShimFileURLToPath(import.meta.url);', + 'const __dirname = __cjsShimDirname(__filename);', + ].join('\n'), + }, + plugins: [rawTextPlugin()], + alias: { + '@': resolve(appRoot, 'src'), + }, + define: { + [BUILT_IN_CATALOG_DEFINE]: builtInCatalogDefine(), + }, + deps: { + onlyBundle: false, + }, + outputOptions: { + codeSplitting: false, + entryFileNames: 'main.mjs', + }, +}); diff --git a/apps/kimi-code/tsdown.dist-worker.config.ts b/apps/kimi-code/tsdown.dist-worker.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..43c26570eb19abfca3a415b298e81108c25e664d --- /dev/null +++ b/apps/kimi-code/tsdown.dist-worker.config.ts @@ -0,0 +1,41 @@ +// Bundles the kap-server global-search worker +// (packages/kap-server/src/search/worker/entry.ts) into ONE self-contained +// `dist/search-worker.mjs` sibling of the main bundle. The search worker +// host resolves it at runtime next to `dist/main.mjs` (dev/tests use the TS +// source; the SEA binary uses the extracted asset from +// tsdown.worker.config.ts). Separate config because rolldown forbids +// `codeSplitting: false` with multiple inputs. + +import { resolve } from 'node:path'; + +import { defineConfig } from 'tsdown'; + +const appRoot = import.meta.dirname; + +export default defineConfig({ + entry: { + 'search-worker': resolve( + appRoot, + '../../packages/kap-server/src/search/worker/entry.ts', + ), + }, + format: ['esm'], + // Shares the main bundle's dist (never wipe it) and lands as + // `dist/search-worker.mjs`. + outDir: 'dist', + clean: false, + dts: false, + hash: false, + platform: 'node', + target: 'node24', + sourcemap: false, + minify: false, + silent: true, + deps: { + onlyBundle: false, + }, + outputOptions: { + codeSplitting: false, + entryFileNames: '[name].mjs', + }, +}); diff --git a/apps/kimi-code/tsdown.native.config.ts b/apps/kimi-code/tsdown.native.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1008cb6165264576ac15a48f6978ce7de9358ef --- /dev/null +++ b/apps/kimi-code/tsdown.native.config.ts @@ -0,0 +1,69 @@ +import { readFileSync } from 'node:fs'; +import { builtinModules } from 'node:module'; +import { resolve } from 'node:path'; + +import { defineConfig } from 'tsdown'; + +import { rawTextPlugin } from '../../build/raw-text-plugin.mjs'; +import { BUILT_IN_CATALOG_DEFINE, builtInCatalogDefine } from './scripts/built-in-catalog.mjs'; + +const appRoot = import.meta.dirname; +const packageJson = JSON.parse( + readFileSync(new URL('./package.json', import.meta.url), 'utf-8'), +) as { version: string }; + +const builtins = new Set([ + ...builtinModules, + ...builtinModules.map((name) => `node:${name}`), +]); +const optionalNativeDependencies = new Set(['cpu-features']); + +function shouldAlwaysBundle(id: string): boolean { + if (builtins.has(id) || id.startsWith('node:')) return false; + if (optionalNativeDependencies.has(id)) return false; + // Everything else is force-bundled, which covers `@moonshot-ai/*` (incl. + // vis-server for `kimi vis`) plus its transitive `hono` / `@hono/node-server` + // — so the SEA bundle is self-contained (check-bundle.mjs enforces this). + return true; +} + +function buildTarget(): string { + return process.env['KIMI_CODE_BUILD_TARGET'] ?? `${process.platform}-${process.arch}`; +} + +export default defineConfig({ + entry: ['./src/main.ts'], + format: ['cjs'], + outDir: 'dist-native/intermediates', + clean: true, + dts: false, + fixedExtension: true, + hash: false, + platform: 'node', + target: 'node24', + banner: { js: '#!/usr/bin/env node' }, + plugins: [rawTextPlugin()], + alias: { + '@': resolve(appRoot, 'src'), + }, + define: { + [BUILT_IN_CATALOG_DEFINE]: builtInCatalogDefine(), + __KIMI_CODE_VERSION__: JSON.stringify(packageJson.version), + __KIMI_CODE_CHANNEL__: JSON.stringify(process.env['KIMI_CODE_CHANNEL'] ?? ''), + __KIMI_CODE_COMMIT__: JSON.stringify(process.env['KIMI_CODE_COMMIT'] ?? ''), + __KIMI_CODE_BUILD_TARGET__: JSON.stringify(buildTarget()), + __KIMI_CODE_NATIVE_BUNDLE__: 'true', + }, + deps: { + alwaysBundle: shouldAlwaysBundle, + neverBundle: [...optionalNativeDependencies], + onlyBundle: false, + }, + outputOptions: { + codeSplitting: false, + entryFileNames: 'main.cjs', + }, + checks: { + legacyCjs: false, + }, +}); diff --git a/apps/kimi-code/tsdown.worker.config.ts b/apps/kimi-code/tsdown.worker.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..34a0109546eb19604949cee10e28ff6557d7b073 --- /dev/null +++ b/apps/kimi-code/tsdown.worker.config.ts @@ -0,0 +1,50 @@ +// Dedicated tsdown config that bundles the off-main-thread workers into +// self-contained ESM files so they can ride the SEA blob as assets +// (02-sea-blob.mjs) and be spawned from disk at runtime: +// - text-build-worker.mjs: the minidb text-build worker +// (packages/minidb/src/worker/text-build-worker.ts); +// - search-worker.mjs: the kap-server global-search worker +// (packages/kap-server/src/search/worker/entry.ts). +// Without them the bundled binary lacks the worker entry files on disk and +// heavy index work degrades to the inline main-thread cores, stalling the +// event loop on large corpora. Runs after the main bundle with clean:false +// so all verified files remain. +// +// One config per entry: rolldown forbids `codeSplitting: false` with +// multiple inputs, and each worker must be a single self-contained file +// (check-bundle.mjs enforces zero remaining externals/relative imports). + +import { resolve } from 'node:path'; + +import { defineConfig } from 'tsdown'; + +const here = import.meta.dirname; + +function workerConfig(name: string, entry: string) { + return defineConfig({ + entry: { [name]: resolve(here, entry) }, + format: ['esm'], + outDir: resolve(here, 'dist-native/intermediates'), + entryFileNames: '[name].mjs', + codeSplitting: false, + platform: 'node', + target: 'node24', + dts: false, + sourcemap: false, + minify: false, + silent: true, + deps: { + // Force-bundle the workspace packages the entries import + // (`@moonshot-ai/minidb` and its subpaths) so the output is + // self-contained. + alwaysBundle: [/^@moonshot-ai\//], + }, + // The intermediates dir also holds main.cjs & co. — never wipe it. + clean: false, + }); +} + +export default [ + workerConfig('text-build-worker', '../../packages/minidb/src/worker/text-build-worker.ts'), + workerConfig('search-worker', '../../packages/kap-server/src/search/worker/entry.ts'), +]; diff --git a/apps/kimi-code/vitest.config.ts b/apps/kimi-code/vitest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..350417bc28728283bdffcd0e4af48c87b50c6237 --- /dev/null +++ b/apps/kimi-code/vitest.config.ts @@ -0,0 +1,20 @@ +import { resolve } from 'node:path'; + +import { defineConfig } from 'vitest/config'; + +const appRoot = import.meta.dirname; + +export default defineConfig({ + resolve: { + alias: { + '@': resolve(appRoot, 'src'), + }, + }, + test: { + name: 'cli', + env: { + KIMI_LOG_LEVEL: 'off', + }, + include: ['test/**/*.test.ts', 'test/**/*.test.tsx'], + }, +}); diff --git a/apps/kimi-inspect/AGENTS.md b/apps/kimi-inspect/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..27698c97833c84343b67af04be5464a8777f9adf --- /dev/null +++ b/apps/kimi-inspect/AGENTS.md @@ -0,0 +1,44 @@ +# kimi-inspect Agent Guide + +Web inspector for the kap-server `/api/v1/debug` RPC surface — workspace/session browser, per-session chat, and Service panels (data + trigger buttons) for the Session and Agent scopes. + +## Top-level views + +A left icon rail (`src/components/NavRail.tsx`) switches top-level views: + +- **Chat workspace** — the per-session chat (see "Chat view" below), with the session tree on the left: `src/components/Sidebar.tsx` is a single-column workspace → session tree over the v2 list's grouped projection (`GET /api/v2/sessions?view=by_workspace`, client in `src/sessions/api.ts` — v1-style `{ code, msg, data }` envelope, opaque-cursor pagination over groups; each workspace group carries its first `group.page_size` sessions plus the full matching total, and a "Show all" row falls back to the flat per-workspace listing). Preset views in `src/sessions/views.ts` map onto the endpoint's status / archived / git query conditions; the active view, collapsed workspaces, and panel width persist to localStorage; live activity badges come from the hub. +- **Global message search** (`src/components/SearchView.tsx`) — cross-session full-text search over `POST /api/v1/search`, cursor-paged via a manual Load more; an exact-match checkbox maps to the API's `mode: 'literal'` substring search, which ignores sort and orders newest-first; a `live`/`index` badge on the results shows which server route served them (in-memory session transcript vs the persisted index). +- **Model Catalog** (`src/components/ModelCatalogView.tsx`) — every Provider with its Models and the default marker, via `IModelCatalog` / `IModelService` channel proxies, with per-model ping and session creation actions. +- **App Services** (`src/components/AppServicesView.tsx`) — the app-scope Service reflection, full width, joined by the **Workspace Services** view (`src/components/WorkspaceServicesView.tsx`) — the workspace-scope counterpart with a left sidebar directory browser (`src/components/WorkspaceDirBrowser.tsx` — server-side fs browsing over the App-scope `IHostFolderBrowser`, marking entries that are registered workspaces with their `IWorkspaceTrust` trust state, and registering a picked folder on demand via `IWorkspaceService.createOrTouch`), its proxies riding the `/workspace/:id` route, which materializes the handler on demand via `IWorkspaceLifecycleService.handlerFor`. +- **DI view** (`src/components/DiInspectionView.tsx`) — the engine's Service × Effect × DI debug surface over the App-scope `IDebugLedgerService` / `IDebugGraphService` / `IDebugEventsService` / `IDebugCascadeService`: the unit tree = ledger tree with unprovide / update / dispose triggers, the dependency DAG as Miller columns (`di/DiGraphPanel.tsx`), the event-subscription ledger (unit-book `on:<name>` entries + per-bus listener counts, `di/DiEventsPanel.tsx`), the cascade history, and the waiting area; the five panels poll on a short interval and refresh eagerly off the global `event.di.unit_changed` WS frame via `src/activity/di.ts`, which invalidates the `['di']` react-query prefix. + +The **Agent scope** stays in the Chat view's right dock (`src/components/RightPanel.tsx` — Audit / Agent / State / Session tabs) across two of them: + +- `Agent` tab — `Inspector`: agent switcher + a Plan lookup card (`PlanCard` in `src/components/Inspector.tsx` — deriving the reviewed plan of one ExitPlanMode tool call, or every plan of the agent, from the message stream: a full `GET /sessions/{id}/history` read via `src/transcript/api.ts`'s `fetchFullHistory` + client-side `projectPlans` in `src/transcript/plan.ts`) plus the agent Service panels. +- `State` tab — every key an Agent Service registered into the agent-state container, polled live via `IAgentStateService.snapshot()` — the same live diff-tree view as the session State tab, sharing `StateCard` from `src/components/StateCard.tsx`. + +The **Session scope** lives in the same right dock as the `Session` tab (`src/components/SessionPane.tsx`, embedded by `RightPanel`) with two sub-tabs: Services (the pending-interactions card — `src/components/InteractionsCard.tsx`, which lists and answers approvals/questions over the public REST endpoints `/api/v1/sessions/{id}/approvals|questions` via `src/interactions/api.ts`, since the interaction kernel is a process-global singleton with no debug channel — plus the session Service panels) and State (every key a Session Service registered into the session-state container, read on demand via `ISessionStateService.snapshot()`). + +## Channel layer + +Built on its own old-klient-style channel layer (`src/channel/`: the VS Code `ProxyChannel` model — service-bound `IChannel`, HTTP `ProxyChannel` for calls routed to `/api/v1/debug`), typed by `agent-core-v2` Service interfaces; `GET /api/v1/debug/channels` loads the whole wire protocol 1:1 (every scoped Service, no whitelist). There is no Service-event push channel: panels fetch/refresh on demand (`Sidebar` polls react-query on a 15 s interval), and a connection failure shows a blocking "Debug surface unavailable" screen instead of falling back anywhere. + +## Session activity + +Session-level coarse status is the one exception to no-push: `src/activity/` holds a second `/api/v1/ws` client (`GlobalEventsWs`) that subscribes to nothing and consumes the server-pushed global facts — `event.session.work_changed` updates a per-session activity map (`SessionActivityHub` + subscribe/version store, seeded on connect/reconnect from `GET /api/v1/sessions`), while `event.session.created` / `event.session.archived` / `session.meta.updated` / `event.workspace.*` invalidate the `['sessions']` / `['v2-sessions']` / `['workspaces']` queries (an archive also drops the session's live activity entry, since no further `work_changed` frames will correct a stale badge); the session tree rows render `running` / `approval` / `question` / `failed` badges from it via `useSessionActivities` (live facts override the REST `activity.status`). + +## Dev server + +The Vite dev server proxies `/api` to a running kap-server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`) and exposes `GET /__inspect/servers` (`vite/serverDiscovery.ts`), which scans the local kap-server instance registry (`~/.kimi-code/server/instances` + legacy `lock`) and the home token so the app can zero-config auto-connect and switch servers from the header dropdown at runtime. + +## Chat view + +The per-session chat (`src/components/ChatView.tsx`) renders turn-granularly from the **message protocol v3** surface and carries an in-chat search bar (`src/components/ChatSearchBar.tsx`): it searches the current session via `POST /api/v1/search` with `container: { session_id }` (usually served by the live route, since selecting a session resumes it), and a result click funnels through the app shell's `openSearchHit` — the same agent-switch + `ChatJump` (page-back, scroll, flash) path the global search view uses. + +Persisted state comes from `GET /api/v1/sessions/{id}/history` only (client in `src/transcript/api.ts`): the initial load reads the newest page (default 500 messages, replace mode), older history auto-pages with `before_turn` via an IntersectionObserver sentinel at the top of the scroll view (prepend mode; a short or empty page ends paging — the response deliberately carries no has-more flag), and each timeline item is wrapped in `content-visibility: auto` + `contain-intrinsic-size` so the browser virtualizes off-screen rendering natively (no windowing library). + +`/api/v3/ws` (client in `src/transcript/ws.ts`) is the live channel: server `hello` → `subscribe {id, session_id, agent_ids: [agent]}` → `ack` → recovery payload (in-flight entities + pending interactions + running tasks + todo + `session.state`) → live traffic, heartbeat at the WS protocol level. The store (`src/transcript/store.ts`) applies recovery and live messages through one idempotent path — entity messages upsert by (type, own id) with content fields authoritative, the delta family (`assistant.delta` / `thinking.delta` / `tool_call.delta`) appends by id, `tool.progress` patches the entity, `system(undo/clear)` truncates the timeline by `payload.removed_ids` (subtree included, linked interactions cascaded), and `interaction` / `task` / `todo` / `session.state` upsert their own single-source maps; an upsert older than the held entity's `timestamp` is skipped. Notifications are throttled trailing-edge so the per-token delta stream does not re-render per token. Every subscribe ack (initial and every reconnect) triggers an `after_step` catch-up from the newest terminal step; an empty catch-up whose anchor vanished (undo/clear while away) falls back to a full refresh. All of this is orchestrated by `ChatChannel` (`src/transcript/channel.ts`) — no buffering, no cursors beyond the two REST page cursors, no reset frames. + +## Transcript audit panel + +The Transcript audit panel (`src/components/audit/`, the `Audit` tab of the chat view's right dock — `src/components/RightPanel.tsx`, fed the trail by `ChatView`'s `onTrailChange`) replays how the visible store was built: an `AuditTrail` (`src/audit/`) records every step — each REST history page (request + replace/prepend/tail mode), every WS message (entity/delta/state as applied), channel events (subscribe ack, reconnect, catch-up fallback, protocol errors), and prompt/cancel actions — with the resulting immutable `ChatState` per entry; the panel offers a draggable timeline plus a Diff tab (structural diff vs the previous entry: added/modified/removed colored, long strings tail-truncated, all fields kept), a full State view (the flat entity timeline plus the interaction/task/todo/session.state entities), and the raw Event payload. diff --git a/apps/kimi-inspect/README.md b/apps/kimi-inspect/README.md new file mode 100644 index 0000000000000000000000000000000000000000..50ca8db5d75420be3dfe33302882b6c9ad24dc59 --- /dev/null +++ b/apps/kimi-inspect/README.md @@ -0,0 +1,57 @@ +# kimi-inspect + +Web inspector for the kap-server `/api/v1/debug` RPC surface — a read/trigger +window into a running Kimi Code engine (workspaces, sessions, agents, and the +scoped DI registry). + +## Run + +1. Start a kap-server with the debug surface mounted (repo dev scripts do this + for you): `pnpm dev:v1` / `pnpm dev:v2` from the repo root pass + `--debug-endpoints` on a loopback bind; the surface inherits the global + bearer auth. +2. `pnpm --filter @moonshot-ai/kimi-inspect dev` — the Vite dev server proxies + `/api` to the server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`) + and auto-discovers running instances + (`~/.kimi-code/server/instances`); switch servers from the header dropdown. + +A connection failure shows a blocking "Debug surface unavailable" screen — +there is no fallback data source. + +## Views (left icon rail) + +- **Chat workspace** — session list (activity badges from the global-events WS) + plus a transcript-driven per-session chat; the right dock hosts the + Agent-scope Service panels, a plan lookup card, and the transcript audit + panel. The Session scope has its own column (pending interactions + session + Service panels, and a State tab). +- **Search** — cross-session full-text search over `POST /api/v1/search` + (cursor-paged; exact-match maps to the API's `literal` mode; a `live`/`index` + badge shows which server route served the results). +- **Model Catalog** — every provider with its models and the default marker, + with per-model ping and session creation actions. +- **App / Workspace Services** — the full Service reflection over the App + scope, and over each Workspace scope (picked via the directory browser; + workspace handlers materialize on demand). +- **DI** — the engine's Service × Effect × DI debug surface, four panels fed + by the App-scope debug Services (`IDebugLedgerService` / `IDebugGraphService` + / `IDebugCascadeService`) and refreshed eagerly off the `event.di.unit_changed` + WS frame: + - **Unit tree** — scope → unit → ledger entries (label, five-state + `Pending / Activating / Active / Unloading / Failed`, uid, `pinned` flag, + unit error object), with **unprovide / update / dispose** triggers. + - **Graph** — the dependency DAG (instance edges across scopes + collection + edges). + - **Cascade** — the cascade transaction history ring (changes, contagion set, + torn-down / rebuilt / failed, abort wait, duration). + - **Pending** — the waiting area: units parked on unsatisfied dependencies + with their missing-token sets. + +## Notes for maintainers + +- The channel layer (`src/channel/`) is a VS Code-style `ProxyChannel`: + `GET /api/v1/debug/channels` enumerates every scoped Service — there is no + whitelist; new Services appear automatically. +- There is no Service-event push channel besides the global events listed + above; panels fetch/refresh on demand (react-query, 15 s poll) plus the + `event.di.unit_changed` invalidation for the DI view. diff --git a/apps/kimi-inspect/index.html b/apps/kimi-inspect/index.html new file mode 100644 index 0000000000000000000000000000000000000000..3a9b3b5e59315c60741aafa4f499381a404f2ce6 --- /dev/null +++ b/apps/kimi-inspect/index.html @@ -0,0 +1,12 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Kimi Inspect + + +
+ + + diff --git a/apps/kimi-inspect/package.json b/apps/kimi-inspect/package.json new file mode 100644 index 0000000000000000000000000000000000000000..ee37ebc9497126f3a890c81d70d66be1dabf4436 --- /dev/null +++ b/apps/kimi-inspect/package.json @@ -0,0 +1,41 @@ +{ + "name": "@moonshot-ai/kimi-inspect", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "imports": { + "#/*": { + "types": [ + "./src/*.ts", + "./src/*.tsx", + "./src/*/index.ts", + "./src/*/index.tsx" + ], + "default": "./src/*" + } + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@moonshot-ai/agent-core-v2": "workspace:^", + "@moonshot-ai/kap-server": "workspace:^", + "@tanstack/react-query": "^5.74.4", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.4", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "tailwindcss": "^4.1.4", + "typescript": "6.0.2", + "vite": "^6.3.3", + "vitest": "4.1.4" + } +} diff --git a/apps/kimi-inspect/tsconfig.json b/apps/kimi-inspect/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..d9490032a814c09a3ebf91c7c7c13be09413bc59 --- /dev/null +++ b/apps/kimi-inspect/tsconfig.json @@ -0,0 +1,22 @@ +{ + // Mirror the repo root tsconfig: agent-core-v2 is consumed through source + // `exports`, so its sources are type-checked here too, and the strictness + // flags must match the root exactly. The src includes are required (not + // just for speed): agent-core-v2 fills interfaces like `AgentTaskInfoByKind` + // via cross-module `declare module` augmentation, and those collapse to + // `never` when the augmenting files are not in the program. Only the + // environment (DOM + vite client types) differs. + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["node", "vite/client"], + "noEmit": true + }, + "include": [ + "src", + "vite", + "vite.config.ts", + "../../packages/agent-core-v2/src" + ], + "exclude": ["../../packages/agent-core-v2/src/human"] +} diff --git a/apps/kimi-inspect/vite.config.ts b/apps/kimi-inspect/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..d72bec71cac458c3ac7155125afec47d6fad328f --- /dev/null +++ b/apps/kimi-inspect/vite.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +import { serverDiscoveryPlugin } from './vite/serverDiscovery'; + +const webPort = Number(process.env['INSPECT_PORT']) || 5176; +// Where the dev proxy forwards server traffic. The app can also connect to an +// arbitrary server URL typed into the connect screen (loopback cross-origin is +// allowed by kap-server), but the default connection is same-origin through +// this proxy so no CORS / Origin handling is involved. +const serverTarget = process.env['KIMI_SERVER_URL'] || 'http://127.0.0.1:58627'; + +export default defineConfig({ + plugins: [react(), tailwindcss(), serverDiscoveryPlugin({ proxyTarget: serverTarget })], + define: { + __KIMI_INSPECT_PROXY_TARGET__: JSON.stringify(serverTarget), + }, + server: { + port: webPort, + strictPort: false, + proxy: { + '/api': { target: serverTarget, changeOrigin: true, ws: true }, + }, + }, + preview: { + port: Number(process.env['INSPECT_PREVIEW_PORT']) || 4176, + proxy: { + '/api': { target: serverTarget, changeOrigin: true, ws: true }, + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + target: 'es2022', + }, +}); diff --git a/apps/vis/package.json b/apps/vis/package.json new file mode 100644 index 0000000000000000000000000000000000000000..9a0c5de2f73479e7c98a39c15949391551faf615 --- /dev/null +++ b/apps/vis/package.json @@ -0,0 +1,17 @@ +{ + "name": "@moonshot-ai/vis", + "version": "0.1.1", + "private": true, + "description": "Debug visualization tool for kimi-code sessions", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "pnpm --filter @moonshot-ai/vis-server build && pnpm --filter @moonshot-ai/vis-web build && node scripts/copy-web-dist.mjs", + "prestart": "pnpm run build", + "start": "node server/dist/server.mjs" + }, + "devDependencies": { + "concurrently": "^9.1.2" + } +} diff --git a/apps/vscode/.vscode-test.mjs b/apps/vscode/.vscode-test.mjs new file mode 100644 index 0000000000000000000000000000000000000000..f728f012c67a180d19a6523f3e8d861e2878668c --- /dev/null +++ b/apps/vscode/.vscode-test.mjs @@ -0,0 +1,5 @@ +import { defineConfig } from "@vscode/test-cli"; + +export default defineConfig({ + files: "out/test/**/*.test.js", +}); diff --git a/apps/vscode/.vscodeignore b/apps/vscode/.vscodeignore new file mode 100644 index 0000000000000000000000000000000000000000..726951204afba930486ac5be64a9f07ce677a338 --- /dev/null +++ b/apps/vscode/.vscodeignore @@ -0,0 +1,50 @@ +# Source files +src/** +webview-ui/** +shared/** +**/*.ts +**/*.tsx + +# Node modules (bundled via esbuild/vite) +**/node_modules/** + +# Build configs +.vscode/** +tsconfig.json +tsdown.config.ts +vitest.config.ts +.vscode-test.mjs +eslint.config.mjs +webview-ui/tsconfig.json +webview-ui/vite.config.ts +webview-ui/components.json + +# Scripts +scripts/** +test/** +runtime/** +artifacts/** + +# Never package generated test/debug state +**/.kimi/** +**/.kimi-code/** +**/cache/** +**/caches/** +**/credentials/** +**/logs/** +**/profile/** +**/profiles/** +**/session/** +**/sessions/** +**/tokens/** +**/*.jsonl +**/*.log + +# Other +.gitignore +**/*.map +*.vsix +**/.DS_Store + +# Prevent parent directory traversal via symlinks +../** diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..2be227645573e7a3091190829ca3524a1f5c95f1 --- /dev/null +++ b/apps/vscode/CHANGELOG.md @@ -0,0 +1,203 @@ +# Changelog + +## 0.7.5 + +### Patch Changes + +- [#3453](https://github.com/MoonshotAI/kimi-code/pull/3453) [`411572e`](https://github.com/MoonshotAI/kimi-code/commit/411572e166edee8581ba9c5a7f1bbf6c8b405606) Thanks [@Grapedge](https://github.com/Grapedge)! - Highlight matched characters in @ file suggestions and allow folders to be inserted as mentions. + +- [#3453](https://github.com/MoonshotAI/kimi-code/pull/3453) [`411572e`](https://github.com/MoonshotAI/kimi-code/commit/411572e166edee8581ba9c5a7f1bbf6c8b405606) Thanks [@Grapedge](https://github.com/Grapedge)! - Show the image/video picker entry in the @ menu only before a search query is typed, and remove the Browse folders mode. + +- [#3453](https://github.com/MoonshotAI/kimi-code/pull/3453) [`411572e`](https://github.com/MoonshotAI/kimi-code/commit/411572e166edee8581ba9c5a7f1bbf6c8b405606) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix the @ and / suggestion lists jittering when the mouse rests at the scroll edge. + +- Updated dependencies [[`411572e`](https://github.com/MoonshotAI/kimi-code/commit/411572e166edee8581ba9c5a7f1bbf6c8b405606)]: + - @moonshot-ai/kimi-code-sdk@0.20.0 + +## 0.7.4 + +### Patch Changes + +- [#3371](https://github.com/MoonshotAI/kimi-code/pull/3371) [`9e88152`](https://github.com/MoonshotAI/kimi-code/commit/9e881528a89945a373002b0b229f91735e8f2c4f) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Fix prompts remaining queued forever after reopening a session. + +- [#3366](https://github.com/MoonshotAI/kimi-code/pull/3366) [`9619277`](https://github.com/MoonshotAI/kimi-code/commit/961927739ef34819d67d76fa5870cbe4ba7a01ff) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Use the Unicode ellipsis "…" in user-facing TUI and VS Code webview text. + +## 0.7.3 + +### Patch Changes + +- [#3098](https://github.com/MoonshotAI/kimi-code/pull/3098) [`75550c5`](https://github.com/MoonshotAI/kimi-code/commit/75550c5686cb867c0cb34bc515997d5e6305fc94) Thanks [@LCZcn96](https://github.com/LCZcn96)! - Restore live context-window usage updates after switching to the v2 engine. + +- [#3276](https://github.com/MoonshotAI/kimi-code/pull/3276) [`f34b2ec`](https://github.com/MoonshotAI/kimi-code/commit/f34b2ecfb01dc194aaa3f209a00772991caad3f3) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix streamed replies occasionally showing every character twice and tool calls appearing in duplicate. + +- Updated dependencies [[`75550c5`](https://github.com/MoonshotAI/kimi-code/commit/75550c5686cb867c0cb34bc515997d5e6305fc94), [`d723cc4`](https://github.com/MoonshotAI/kimi-code/commit/d723cc47ee43e5ca3c3c4ec2473f205d44acede2)]: + - @moonshot-ai/kimi-code-sdk@0.19.2 + +## 0.7.2 + +### Patch Changes + +- [#3079](https://github.com/MoonshotAI/kimi-code/pull/3079) [`35befdc`](https://github.com/MoonshotAI/kimi-code/commit/35befdcef2be344d931ea20063cb64113350dc4b) Thanks [@gaoyuan1223m](https://github.com/gaoyuan1223m)! - Fix multi-select questions jumping to the next question after only one answer is selected. + +- Updated dependencies [[`3d77620`](https://github.com/MoonshotAI/kimi-code/commit/3d7762003a4a35cbeb8571d471c6898a006152e6)]: + - @moonshot-ai/kimi-code-sdk@0.19.1 + +## 0.7.1 + +### Patch Changes + +- [#3026](https://github.com/MoonshotAI/kimi-code/pull/3026) [`13857f3`](https://github.com/MoonshotAI/kimi-code/commit/13857f383200881aa77dc972a8963ba421eeb2b6) Thanks [@bj456736](https://github.com/bj456736)! - Show plugin- and file-declared MCP servers as read-only entries in the MCP servers panel. + +- Updated dependencies [[`d833a1a`](https://github.com/MoonshotAI/kimi-code/commit/d833a1a893c4d69d96af542f40557442992085e0), [`61591bc`](https://github.com/MoonshotAI/kimi-code/commit/61591bce09f4467aa1664cb8ecb6aa6904b7accd), [`d833a1a`](https://github.com/MoonshotAI/kimi-code/commit/d833a1a893c4d69d96af542f40557442992085e0), [`13857f3`](https://github.com/MoonshotAI/kimi-code/commit/13857f383200881aa77dc972a8963ba421eeb2b6)]: + - @moonshot-ai/kimi-code-sdk@0.19.0 + +## 0.7.0 + +### Minor Changes + +- [#2916](https://github.com/MoonshotAI/kimi-code/pull/2916) [`7475c2e`](https://github.com/MoonshotAI/kimi-code/commit/7475c2e2e3dd86ac0b8a8d51d4f1d233ed7df797) Thanks [@Grapedge](https://github.com/Grapedge)! - Run the extension on the v2 agent engine by default; the interface, sessions, and workflows are unchanged. To roll back, enable the `kimi.useAgentCoreV1` setting and reload the window. + +### Patch Changes + +- Updated dependencies [[`6be2697`](https://github.com/MoonshotAI/kimi-code/commit/6be26978b123bacf1c5ebce52bbeb6f7b7ff0629), [`7475c2e`](https://github.com/MoonshotAI/kimi-code/commit/7475c2e2e3dd86ac0b8a8d51d4f1d233ed7df797), [`7475c2e`](https://github.com/MoonshotAI/kimi-code/commit/7475c2e2e3dd86ac0b8a8d51d4f1d233ed7df797), [`7475c2e`](https://github.com/MoonshotAI/kimi-code/commit/7475c2e2e3dd86ac0b8a8d51d4f1d233ed7df797)]: + - @moonshot-ai/kimi-code-sdk@0.18.0 + +## 0.6.9 + +### Patch Changes + +- Updated dependencies [[`c9bfe8b`](https://github.com/MoonshotAI/kimi-code/commit/c9bfe8b2c8314ba4ef8806fb3b92ac654c1d1860), [`c212ae9`](https://github.com/MoonshotAI/kimi-code/commit/c212ae9715371c0d7939c15e664acbe0d7cf7fc3)]: + - @moonshot-ai/kimi-code-sdk@0.17.0 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies [[`437a1b8`](https://github.com/MoonshotAI/kimi-code/commit/437a1b8ba1b7e0f6662bdadc669564fdc58c3f5a), [`0b2e803`](https://github.com/MoonshotAI/kimi-code/commit/0b2e803d5e71afaab45212bb2ee6117ecbf8bbc9), [`3c9e3b2`](https://github.com/MoonshotAI/kimi-code/commit/3c9e3b297cf5286c761159c1b4d642c478fd394d)]: + - @moonshot-ai/kimi-code-sdk@0.16.0 + +## 0.6.7 + +### Patch Changes + +- [#2326](https://github.com/MoonshotAI/kimi-code/pull/2326) [`302b2cd`](https://github.com/MoonshotAI/kimi-code/commit/302b2cd680e0ec66f68b4572238de84ce311c5f4) Thanks [@gaoyuan1223m](https://github.com/gaoyuan1223m)! - Fix only the first question being answerable when the agent asked multiple questions at once; each question is now answered one by one and submitted together. + +## 0.6.6 + +### Patch Changes + +- [#2393](https://github.com/MoonshotAI/kimi-code/pull/2393) [`6d0a046`](https://github.com/MoonshotAI/kimi-code/commit/6d0a046488edda56219961b253c4787abae7a113) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix new users getting stranded on "Model setup required" with no way back to sign-in when the first login finishes authorization but fails to complete model setup; the screen now offers a path back to the sign-in page so login can be retried. +- [#2402](https://github.com/MoonshotAI/kimi-code/pull/2402) [`0f3b106`](https://github.com/MoonshotAI/kimi-code/commit/0f3b106c4260ad626f66bc5c457a535d3163f2bc) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Reword the sign-in waiting message from "Waiting for authorization" to "Waiting for authentication". + +- Updated dependencies [[`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa)]: + - @moonshot-ai/kimi-code-sdk@0.15.0 + +## 0.6.5 + +### Patch Changes + +- [#1994](https://github.com/MoonshotAI/kimi-code/pull/1994) [`beeb964`](https://github.com/MoonshotAI/kimi-code/commit/beeb964393c8f9a38c2b1e2273e4415fc434b16d) Thanks [@RealKai42](https://github.com/RealKai42)! - Reduce webview streaming re-render churn: settled assistant messages no longer re-render on every streaming delta, and local images over 10MB are no longer inlined into the webview DOM. +- Updated dependencies [[`ec88d35`](https://github.com/MoonshotAI/kimi-code/commit/ec88d352e8f4dc5e8ffd1212f016138458f69893), [`b5efba7`](https://github.com/MoonshotAI/kimi-code/commit/b5efba7abcaf4041f81ec520097a61e6546e8c50), [`ce0e3ce`](https://github.com/MoonshotAI/kimi-code/commit/ce0e3ceb04223bdaad8e8931bad46eff561055b6), [`e458323`](https://github.com/MoonshotAI/kimi-code/commit/e45832398d0d9cad98dbad1cbf1e5b103a20aace)]: + - @moonshot-ai/kimi-code-sdk@0.14.0 + +## 0.6.4 + +### Changed + +- Picking a model's highest thinking effort now applies to the current session + only instead of becoming the global default: the top tier saves just the + on/off toggle, lower tiers persist as the default as before, and + re-confirming the current effort no longer rewrites the saved preference. + The model and thinking pickers also note that switching mid-conversation + invalidates the existing prompt cache. +- Unified the YOLO and Auto permission mode naming and descriptions with the + CLI (`/afk` is now `/auto`), and approval requests that fall outside the + active permission mode (sensitive files, plan reviews, ask rules) are now + always shown to you instead of being auto-approved. + +## 0.6.3 + +### Fixed + +- Editor mentions now work for files outside the working directory, and paths + containing spaces are quoted correctly. +- Cancelling a running turn now reliably reaches the engine, and the UI no + longer reports a task as stopped when there is nothing to cancel. +- Attaching to or resuming an existing session no longer overwrites its model + and thinking effort with the configured defaults; model or effort changes + picked in the composer are applied when the prompt is sent. + +## 0.6.2 + +### Fixed + +- A core error arriving in the middle of a turn no longer corrupts the active + turn; the turn now ends cleanly with an error instead of leaving the chat in + a broken state. +- Kimi sign-in and connection failures now include the underlying transport + cause (for example DNS or connection refused) instead of a generic error. +- Closed several FetchURL SSRF bypasses and the DNS-rebinding window. +- Tool calls interrupted mid-stream are now recorded and closed, so they no + longer corrupt the session history. + +## 0.6.1 + +### Fixed + +- The **Sign in** action in the settings (gear) menu now actually starts the + Kimi login flow and shows an error toast when sign-in fails, instead of + silently doing nothing. + +## 0.6.0 + +### Breaking + +- Raised the minimum supported editor version to VS Code 1.100.0. +- Legacy Kimi Code OAuth credentials and MCP OAuth credentials are deliberately + not migrated. Sign in to Kimi Code again and re-authorize affected MCP + servers after upgrading. +- Removed the `kimi.executablePath` and `kimi.environmentVariables` settings. + The old `kimi.environmentVariables.KIMI_SHARE_DIR` value is consulted only to + discover legacy data during migration; it is not applied to the new runtime. + The system-level `KIMI_CODE_HOME` environment variable remains supported. + +### Changed + +- Replaced the legacy Python/stdio runtime with the in-process Kimi Code Node + SDK. The extension no longer downloads or starts a separate Kimi executable. +- The in-process engine is the same one that powers the Kimi Code CLI, so the + agent gains CLI-parity capabilities beyond the legacy runtime, including + parallel subagent swarms, background tasks, and long-running goal runs. +- Added an opt-in legacy migration prompt on the first launch that detects data + from version 0.5.x. The migration copies or merges supported data into the + current Kimi Code home and does not delete the legacy source. If migration is + skipped or needs to be retried, run **Kimi Code: Migrate Legacy Data** from the + Command Palette. +- When VS Code and the Kimi Code terminal app resolve to the same + `KIMI_CODE_HOME`, they use the same configuration and session storage. Running + the same session concurrently from multiple processes is not supported or + protected by cross-process locking. +- The model picker groups models by provider when multiple providers are + configured, keeps provider identity when display names match, and recognizes + adaptive-thinking metadata. A configured custom default provider no longer + requires dismissing the Kimi account login screen on every launch. +- The file changes panel and Undo actions use extension-maintained baselines. + Files changed through Kimi's Write and Edit operations are tracked on a + best-effort basis. File deletions performed inside Bash are not tracked by + this baseline and therefore cannot be restored by the panel's Undo action. + +### Fixed + +- The `kimi.yoloMode` setting now reaches the permission engine: enabling it + maps to the core `yolo` permission mode and takes effect when a session + attaches, including sessions that previously stored a disabled auto-approve + state. +- Kept the chat header and input toolbar readable when the sidebar is narrow: + controls wrap and shrink instead of being clipped. + +### Distribution boundary + +Release packaging produces target-specific VSIX files for `darwin-x64`, +`darwin-arm64`, `linux-x64`, `linux-arm64`, `win32-x64`, and `win32-arm64`. +Archive and static verification for a target does not by itself prove that the +extension has run successfully in that target's Extension Host; runtime test +results must be recorded separately for each operating system and architecture. diff --git a/apps/vscode/LICENSE b/apps/vscode/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..7a4a3ea2424c09fbe48d455aed1eaa94d9124835 --- /dev/null +++ b/apps/vscode/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/apps/vscode/README.md b/apps/vscode/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ef73e424909545ccf46a59fed01a9d4a98990b0a --- /dev/null +++ b/apps/vscode/README.md @@ -0,0 +1,43 @@ +# Kimi Code + +AI coding assistant for VS Code, built for long-context workflows and complex coding tasks. + +## Features + +- **Works alongside you**: Kimi autonomously explores your codebase, reads and writes code, and runs terminal commands with your permission +- **Thinking controls**: Toggle reasoning or choose a model-supported thinking effort +- **Provider-aware models**: Distinguish and select same-named models across configured providers +- **Native editor integration**: Review AI-proposed changes directly in VS Code's diff viewer +- **MCP support**: Extend capabilities with Model Context Protocol servers +- **Slash commands**: Quick actions like `/init` to analyze your project and `/compact` to manage context + +## Install + +Kimi Code requires VS Code 1.100.0 or later. + +1. Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=moonshot-ai.kimi-code) +2. Open a folder in VS Code +3. Click the Kimi icon in the Activity Bar +4. Sign in with a [kimi.com/code](https://www.kimi.com/code) subscription, or use a provider already configured in the shared `config.toml` + +The extension runs the Kimi Code Node SDK in the VS Code Extension Host. When +the extension and the Kimi Code terminal app resolve to the same +`KIMI_CODE_HOME`, they share `config.toml`, MCP configuration, login state, and +sessions. The system-level `KIMI_CODE_HOME` environment variable is supported; +there is no separate VS Code setting for it. Do not run the same session from +both applications at the same time, because cross-process session locking is +not guaranteed. + +After upgrading from version 0.5.x, the extension prompts before migrating any +legacy data it finds. Migration copies or merges data into the current Kimi Code +home and does not delete the legacy source. Legacy Kimi Code OAuth and MCP OAuth +credentials are not copied, so those connections must be authorized again. +See [the changelog](CHANGELOG.md) for the full compatibility notes. + +## Docs + +Official doc for Kimi Code can be found at [www.kimi.com/code/docs](https://www.kimi.com/code/docs/en/kimi-code-for-vscode/guides/getting-started.html) + +## License + +[Apache-2.0](LICENSE) diff --git a/apps/vscode/docs/node-sdk-migration.md b/apps/vscode/docs/node-sdk-migration.md new file mode 100644 index 0000000000000000000000000000000000000000..055bb0089a51f1f380ce67753a476d3c3bf50ed8 --- /dev/null +++ b/apps/vscode/docs/node-sdk-migration.md @@ -0,0 +1,396 @@ +# VS Code Node SDK Migration Design + +Status: accepted and implemented for extension `0.6.0` + +Last updated: 2026-07-16 + +## Context + +The `0.5.x` VS Code extension launched a separately installed Python Kimi CLI +and communicated with it over stdio. That architecture duplicated runtime +installation, configuration, authentication, and session behavior between the +editor and Kimi Code. + +Version `0.6.0` moves the extension into this monorepo under `apps/vscode` and +runs the stable TypeScript v1 engine through `@moonshot-ai/kimi-code-sdk` in the +VS Code Extension Host. The migration preserves the existing extension ID, +commands, Webview, and user-visible workflows. It does not redesign the UI or +introduce unrelated TUI features. + +This document records the durable design decisions behind that migration. It +is not a release checklist or a transcript of the implementation process. + +## Goals + +- Keep the extension ID `moonshot-ai.kimi-code` so `0.6.0` upgrades existing + installations. +- Preserve the existing VS Code commands, shortcuts, Webview workflows, editor + integration, session management, MCP management, and file changes panel. +- Replace the Python/stdio host with the in-process v1 Node SDK. +- Share Kimi Code configuration, authentication, MCP configuration, and + sessions with the TUI when both processes resolve the same Kimi Code home. +- Reuse the shared legacy migration package instead of maintaining a VS + Code-specific session translator. +- Add only the smallest SDK/core APIs needed to preserve existing VS Code + behavior. +- Package and test platform-targeted VSIX artifacts for macOS, Linux, and + Windows. + +The only intentional UI capability added during the migration is +model-aware thinking effort selection. It is necessary to represent the model +capabilities exposed by the v1 configuration. + +## Non-goals + +- Replacing the React Webview with VS Code native views or Chat Participants. +- Migrating to the v2 engine. +- Adding TUI-only features such as goals, cron, swarm, or BTW. +- Keeping a Python CLI fallback or a custom executable setting. +- Adding cross-process locks for concurrent access to one session. +- Making the core wait for VS Code before file tools execute. +- Parsing arbitrary shell commands to infer file changes. +- Copying legacy OAuth or MCP OAuth credentials. +- Publishing an extension as part of the build or package commands. + +## Runtime architecture + +```mermaid +flowchart LR + UI["React Webview
browser sandbox"] + Host["VS Code Extension Host
Node process"] + SDK["@moonshot-ai/kimi-code-sdk
KimiHarness and Session"] + Core["agent-core-v2"] + Home["Kimi Code home
config, auth, MCP, sessions"] + + UI <-->|"postMessage RPC and events"| Host + Host -->|"in-process calls"| SDK + SDK -->|"in-process calls"| Core + SDK <--> Home +``` + +There is no Python process, secondary Node process, or local HTTP server in the +production extension path. + +The Webview remains because it is the existing product UI. It cannot import the +Node SDK directly: the Webview is a browser sandbox and must not access the file +system, credentials, process environment, or session storage. Those operations +stay in the trusted Extension Host. + +### Extension identity + +The runtime constructs the SDK client with: + +- `productName: "kimi-code-vscode"` +- `version` from `apps/vscode/package.json` +- `uiMode: "vscode"` + +For `0.6.0`, the normal HTTP User-Agent product is therefore +`kimi-code-vscode/0.6.0`. The version has one source of truth and is not copied +into runtime code or packaging scripts. + +### Package boundaries + +- `apps/vscode` depends on `@moonshot-ai/kimi-code-sdk`. +- `apps/vscode` must not depend directly on engine packages. +- Core capabilities needed by released clients are exposed through the Node SDK + and tested at that public boundary. +- The Webview communicates only through the typed bridge in + `apps/vscode/shared`. + +## Main components + +| Area | Primary implementation | +|---|---| +| Activation and VS Code commands | `apps/vscode/src/extension.ts` | +| Webview lifecycle | `apps/vscode/src/KimiWebviewProvider.ts` | +| Webview RPC boundary | `apps/vscode/src/bridge-handler.ts`, `apps/vscode/src/handlers` | +| SDK host | `apps/vscode/src/runtime/kimi-runtime.ts` | +| Session lifecycle and event routing | `apps/vscode/src/runtime/session-runtime.ts` | +| SDK-to-Webview event conversion | `apps/vscode/src/runtime/event-adapter.ts` | +| Session replay | `apps/vscode/src/runtime/replay-adapter.ts` | +| File changes and baselines | `apps/vscode/src/managers` | +| Legacy migration coordination | `apps/vscode/src/migration` | +| React UI | `apps/vscode/webview-ui` | +| Packaging and smoke tests | `apps/vscode/scripts`, `.github/workflows/ci.yml` | + +## Data ownership + +### Shared Kimi Code home + +The SDK resolves the home directory using the normal Kimi Code rules: + +1. system-level `KIMI_CODE_HOME`, when set; +2. otherwise `~/.kimi-code`. + +The extension does not add a separate `kimi.homeDir` setting and does not pass +its own default home to the SDK. VS Code and the TUI share the following data +only when they resolve the same home: + +- `config.toml` +- `mcp.json` +- authentication state +- `sessions/` +- `session_index.jsonl` +- other SDK-owned Kimi Code data + +Remote SSH, WSL, and Dev Container installations use the environment and home +of the remote Extension Host. They do not automatically share the local +machine's Kimi Code home. + +### VS Code-owned state + +VS Code settings and Extension Host storage own editor-specific behavior: + +- autosave and keyboard behavior; +- thinking display preferences; +- editor context injection mode; +- Webview state; +- file change baselines and legacy baseline acceptance markers. + +These values are not written into the shared core configuration unless they +are already a shared product setting, such as the selected model or thinking +effort. + +### Environment variables + +The old `kimi.environmentVariables` setting existed to populate the environment +of the Python child process. It was removed with that process model. + +- Provider-specific environment variables remain in `config.toml`. +- MCP server environment variables remain in `mcp.json`. +- proxy and other process-level variables are inherited from the Extension + Host environment. +- a legacy `KIMI_SHARE_DIR` in the removed setting is consulted only as an + additional migration source. +- other values from the removed global environment map are not migrated. + +## Webview bridge + +The bridge keeps the request/response and event-broadcast shape of the previous +extension so the React UI did not require a product redesign. The Extension +Host validates method names, payloads, workspace containment, and file paths. +The Webview uses a nonce-based content security policy and does not receive +tokens, complete configuration files, or unnecessary absolute paths. + +The event adapter maps v1 SDK events such as `turn.started`, +`assistant.delta`, `tool.call.started`, approvals, questions, compaction, and +errors into the UI state expected by the migrated Webview. Session replay uses +the SDK/core replay surface rather than parsing current core storage directly +from the UI. + +Provider errors may arrive after a failed turn-end event. Session subscriptions +therefore remain alive long enough to deliver the final error instead of being +cancelled immediately when a turn ends. + +## Sessions + +The Node SDK exposes the session operations required by the existing editor UI: + +- create, resume, list, rename, and export; +- permanent delete; +- fork from a selected historical turn; +- replayed context and records needed to restore the UI; +- approval, question, stop, steer, plan mode, model, and thinking controls. + +Permanent delete keeps the old VS Code meaning; it is not silently mapped to +archive. A running target session is stopped and closed before its persisted +data and index references are removed. + +Historical fork keeps the selected turn and its preceding context, then removes +later conversation state. Baselines are materialized into the fork so Keep or +Undo actions in one session cannot affect the other. + +The TUI and VS Code may resume sessions created by each other. They must not run +the same session concurrently because the v1 store has no cross-process write +lock. + +## Provider-aware models and thinking + +Model aliases retain their provider identity. The picker groups models by +provider when multiple providers are configured, so same-named models remain +distinct. Media fallback prefers a compatible model from the current provider +before considering another provider. + +Thinking controls follow each model's declared capabilities: + +- models with `support_efforts` expose those effort values; +- boolean-thinking models expose on/off; +- `always_thinking` models do not expose an invalid off state; +- adaptive-thinking capability is preserved when applying the selection. + +Changing the model or thinking effort updates the active session and the shared +default configuration used by new TUI and VS Code sessions. + +## MCP + +Home-level MCP operations are exposed by the SDK harness rather than by direct +file access from the extension. They cover configuration CRUD, OAuth/reset, +connection testing, and user-global `mcp.json` entries. Session-level status and +reconnect behavior remain on the Session surface. + +The extension supports stdio and HTTP servers, including their environment, +headers, and authorization flows. Legacy MCP OAuth credentials are not copied +and may require authorization after upgrade. + +## Legacy migration + +Migration is opt-in and uses `@moonshot-ai/migration-legacy` for detection and +translation. The extension coordinates prompts and reports but does not +maintain another config/session translator. + +### Sources and target + +- default source: `~/.kimi`; +- optional additional source: a valid legacy `KIMI_SHARE_DIR` from the removed + VS Code setting; +- target: the SDK-resolved Kimi Code home. + +Migration covers the shared config, MCP config, user history, supported skills, +and sessions. Existing target data wins according to the shared migration +package's conflict rules. Migration is repeatable and does not delete the +legacy source. + +On first launch, the extension detects work without mutating either home and +offers **Migrate now** or **Later**. The command +`Kimi Code: Migrate Legacy Data` remains available for manual runs and retries. + +The shared marker `.migrated-to-kimi-code` can contain multiple target homes. +This prevents duplicate migration when the TUI migrated the same source first, +while still allowing a different `KIMI_CODE_HOME` to be migrated later. + +Migrated sessions keep source metadata in `state.json.custom`, including the +legacy source path and session identity. This metadata also supports legacy +baseline fallback. + +OAuth and MCP OAuth credentials are intentionally not copied. Refresh tokens +may rotate, so copying them can invalidate one installation or create ambiguous +ownership. The upgrade flow and release notes must tell users to authorize +again when needed. + +## File changes and baselines + +The baseline feature exists only to support the VS Code File Changes panel, +diff view, Keep Changes, and Undo. It is not core session state and is not a TUI +feature. + +### New sessions + +Baselines are stored under the extension's `globalStorage`, namespaced by the +resolved Kimi Code home and session ID. The home namespace prevents sessions +with the same ID in different homes from sharing baseline state. + +The session runtime observes `tool.call.started` for the explicit `Write` and +`Edit` tools. It captures the first pre-change content for each file and +refreshes the File Changes panel after tool results. A missing original file is +represented as a newly created file. + +- **Keep** removes the effective baseline entry from the panel. +- **Undo** restores the original content or deletes a file that did not exist. +- keeping a file and editing it again starts a new baseline period. +- all paths are resolved against the session work directory and checked for + traversal and symlink escape. + +This is intentionally best-effort. The core does not pause tool execution for a +VS Code callback, and arbitrary Bash file mutations are not tracked. + +### Migrated sessions + +Legacy baselines are not copied in bulk. A migrated session uses its recorded +legacy source path and reads `/baseline` as a read-only +fallback. + +The lookup order is: + +1. extension-owned baseline; +2. an extension-owned marker saying a legacy path was accepted; +3. the legacy baseline. + +Keeping a legacy change writes the acceptance marker instead of deleting old +data. Forking materializes the currently effective baseline into the target +session's extension storage. If the user deletes the legacy home, the migrated +conversation remains usable but legacy diff/Undo information is no longer +available. + +## Packaging and CI + +The Webview and Extension Host are separate build products: + +- Vite builds browser assets for the Webview. +- the Extension Host bundle includes the Node SDK and v1 runtime; only VS Code + host modules remain external. + +The production VSIX excludes tests, fixtures, source maps, local profiles, +sessions, caches, logs, tokens, and the old Python/stdio runtime. Package audit +checks the manifest, assets, unresolved runtime imports, and sensitive files +after test state has been created. + +The extension produces six target artifacts: + +- `darwin-x64`, `darwin-arm64` +- `linux-x64`, `linux-arm64` +- `win32-x64`, `win32-arm64` + +The CI matrix builds and audits the target VSIX files. Installed-extension smoke +tests run on Linux, macOS, and Windows x64 runners. Linux checks the declared +minimum VS Code version and stable; macOS and Windows check the declared +minimum. Architecture targets without a matching runner remain package/audit +evidence rather than runtime E2E evidence. + +Packaging never publishes. Marketplace and Open VSX publication require a +separate authorized release action. + +## Validation snapshot + +The migration was validated with: + +- package-local tests for the extension, Node SDK, v1 core, and legacy + migration; +- real temporary homes and workspaces for migration and baseline behavior; +- provider-aware model, identity, MCP, error, session replay, delete, and fork + contract tests; +- production Extension Host and Webview builds; +- six target VSIX package audits; +- an upgrade from the released `0.5.10` extension to local `0.6.0` in an + isolated profile; +- installed VSIX Extension Host smoke on Linux, macOS, and Windows x64 CI; +- local installed `darwin-arm64` VSIX smoke on VS Code `1.100.0`; +- repository lint, typecheck, tests, Nix build, workspace sync, changeset + status, and whitespace checks. + +All automated CI checks for the migration branch were green when this document +was finalized. + +## Known limitations and release gates + +- Remote SSH, WSL, and Dev Container behavior follows `extensionKind: + ["workspace"]` and remote home resolution, but still needs representative + release-candidate smoke in a real remote environment. +- Cursor installation, activation, Webview, and basic chat require a usable + Cursor environment; VS Code smoke is not a substitute. +- A release candidate should receive one real-account auth/provider smoke in a + workspace containing no sensitive files. +- Migrated nested subagents preserve the parent call/summary but cannot recreate + every child prompt, step, tool, and approval detail from the legacy format. +- Legacy OAuth and MCP OAuth require reauthorization. +- Bash-based deletes and arbitrary external file edits are outside baseline + tracking. +- Concurrent writes to one session from multiple processes are unsupported. +- The package script uses a pinned `@vscode/vsce` programmatic `pack()` entry; + upgrading that package requires revalidating the packaging contract. + +## Maintenance invariants + +Future changes must preserve these boundaries unless a new design explicitly +replaces them: + +1. The Webview never imports the Node SDK or gains direct Node/file/auth access. +2. `apps/vscode` never imports engine packages directly. +3. Shared config and sessions live in the SDK-resolved Kimi Code home; editor + preferences and baselines remain VS Code-owned. +4. Legacy migration translation stays in `packages/migration-legacy`. +5. Session storage is accessed through SDK/core APIs, not parsed or mutated by + the Webview. +6. Baselines remain an extension compatibility layer and do not make core tool + execution wait for VS Code. +7. Package-only evidence is not reported as runtime E2E evidence. +8. Build/package commands do not publish artifacts. diff --git a/apps/vscode/package.json b/apps/vscode/package.json new file mode 100644 index 0000000000000000000000000000000000000000..4dc3c879daddfcec7ad8f6b03ef28adf02530dad --- /dev/null +++ b/apps/vscode/package.json @@ -0,0 +1,321 @@ +{ + "name": "kimi-code", + "publisher": "moonshot-ai", + "displayName": "Kimi Code", + "description": "Official Kimi Code plugin for VS Code", + "version": "0.7.5", + "private": true, + "license": "Apache-2.0", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/MoonshotAI/kimi-code.git", + "directory": "apps/vscode" + }, + "engines": { + "vscode": "^1.100.0" + }, + "extensionKind": [ + "workspace" + ], + "capabilities": { + "untrustedWorkspaces": { + "supported": false + }, + "virtualWorkspaces": false + }, + "categories": [ + "AI", + "Chat", + "Programming Languages" + ], + "keywords": [ + "ai", + "assistant", + "code", + "chat", + "agent", + "mcp", + "skills", + "json", + "autocomplete", + "moonshot", + "kimi", + "moonshot-ai" + ], + "pricing": "Free", + "activationEvents": [], + "main": "./dist/extension.js", + "icon": "resources/kimi-icon-storefront.png", + "contributes": { + "configuration": { + "title": "Kimi Code", + "properties": { + "kimi.yoloMode": { + "type": "boolean", + "default": false, + "description": "Auto-approve regular tool calls; the agent may still ask questions" + }, + "kimi.autosave": { + "type": "boolean", + "default": true, + "description": "Automatically save files before Kimi reads or writes them" + }, + "kimi.enableNewConversationShortcut": { + "type": "boolean", + "default": false, + "description": "Use Cmd/Ctrl+N to start a new conversation when Kimi is focused" + }, + "kimi.useCtrlEnterToSend": { + "type": "boolean", + "default": false, + "description": "Use Ctrl/Cmd+Enter to send prompts instead of Enter" + }, + "kimi.showThinkingContent": { + "type": "boolean", + "default": true, + "description": "Show thinking/reasoning content in the chat UI" + }, + "kimi.showThinkingExpanded": { + "type": "boolean", + "default": false, + "description": "Auto-expand thinking/reasoning sections when shown (requires 'Show Thinking Content' to be enabled)" + }, + "kimi.editorContext": { + "type": "string", + "default": "never", + "enum": [ + "never", + "onConversationStart", + "onFileChange" + ], + "enumDescriptions": [ + "Never share editor context", + "Share once when conversation starts", + "Share when active file changes" + ], + "description": "Control when to share the active editor's file and cursor position with Kimi" + } + } + }, + "commands": [ + { + "command": "kimi.clearAllState", + "title": "Kimi Code: [DEBUG] Clear All State" + }, + { + "command": "kimi.openInTab", + "title": "Kimi Code: Open in New Tab", + "icon": "$(link-external)" + }, + { + "command": "kimi.openInSideBar", + "title": "Kimi Code: Open in Side Panel", + "icon": "$(layout-sidebar-left)" + }, + { + "command": "kimi.focusInput", + "title": "Kimi Code: Focus Input", + "icon": "$(edit)" + }, + { + "command": "kimi.insertMention", + "title": "Kimi Code: Insert Current File", + "icon": "$(mention)" + }, + { + "command": "kimi.newConversation", + "title": "Kimi Code: New Conversation", + "icon": "$(add)" + }, + { + "command": "kimi.showLogs", + "title": "Kimi Code: Show Logs", + "icon": "$(output)" + }, + { + "command": "kimi.resetKimi", + "title": "Kimi Code: Reset Kimi (in case of any issues/no response)", + "icon": "$(refresh)" + }, + { + "command": "kimi.logout", + "title": "Kimi Code: Logout", + "icon": "$(sign-out)" + }, + { + "command": "kimi.migrateLegacyData", + "title": "Kimi Code: Migrate Legacy Data" + } + ], + "keybindings": [ + { + "command": "kimi.focusInput", + "key": "ctrl+shift+k", + "mac": "cmd+shift+k" + }, + { + "command": "kimi.insertMention", + "key": "alt+k", + "mac": "alt+k", + "when": "editorTextFocus" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "kimi-sidebar", + "title": "Kimi Code", + "icon": "resources/kimi-icon.svg" + } + ] + }, + "views": { + "kimi-sidebar": [ + { + "type": "webview", + "id": "kimi.webview", + "name": "Kimi Code" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "kimi.openInTab", + "when": "view == kimi.webview", + "group": "navigation" + }, + { + "command": "kimi.newConversation", + "when": "view == kimi.webview", + "group": "navigation" + } + ], + "commandPalette": [ + { + "command": "kimi.clearAllState", + "when": "isDevelopment" + }, + { + "command": "kimi.openInTab", + "when": "true" + }, + { + "command": "kimi.openInSideBar", + "when": "true" + }, + { + "command": "kimi.focusInput", + "when": "true" + }, + { + "command": "kimi.insertMention", + "when": "true" + }, + { + "command": "kimi.newConversation", + "when": "true" + }, + { + "command": "kimi.showLogs", + "when": "true" + }, + { + "command": "kimi.resetKimi", + "when": "true" + }, + { + "command": "kimi.logout", + "when": "kimi.isLoggedIn" + }, + { + "command": "kimi.migrateLegacyData", + "when": "true" + } + ], + "editor/context": [ + { + "command": "kimi.insertMention", + "group": "moonshot-ai", + "when": "editorTextFocus" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "pnpm run build", + "dev": "pnpm run dev:prepare && pnpm run \"/^dev:(extension|webview)$/\"", + "dev:prepare": "node scripts/prepare-dev.mjs", + "dev:extension": "node scripts/watch-extension.mjs", + "dev:webview": "vite build --config webview-ui/vite.config.ts --watch --sourcemap", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p webview-ui/tsconfig.json --noEmit", + "build": "pnpm run build:extension && pnpm run build:webview", + "build:webview": "vite build --config webview-ui/vite.config.ts", + "build:extension": "tsdown --config tsdown.config.ts", + "test": "vitest run --config vitest.config.ts", + "test:extension-host": "node scripts/extension-host-smoke.mjs", + "package:platform": "node scripts/vsix-package.mjs", + "package:verify": "node scripts/vsix-verify.mjs", + "publish:vsix": "node scripts/vsix-publish.mjs", + "publish:ovsx": "node scripts/ovsx-publish.mjs" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.6", + "@types/diff": "^8.0.0", + "@types/katex": "^0.16.8", + "@types/node": "^22.15.3", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@types/react-scroll-to-bottom": "^4.2.5", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/vscode": "1.100.0", + "@vitejs/plugin-react": "^4.4.1", + "@vscode/test-cli": "^0.0.11", + "@vscode/test-electron": "^2.5.2", + "@vscode/vsce": "3.9.2", + "acorn": "8.17.0", + "jsdom": "^30.0.1", + "ovsx": "1.0.2", + "tailwindcss": "^4.1.4", + "vite": "^6.3.3", + "vite-plugin-css-injected-by-js": "^3.5.2", + "vitest": "4.1.4" + }, + "dependencies": { + "@base-ui/react": "^1.0.0", + "@fontsource-variable/inter": "5.2.8", + "@moonshot-ai/kimi-code-oauth": "workspace:^", + "@moonshot-ai/kimi-code-sdk": "workspace:^", + "@moonshot-ai/migration-legacy": "workspace:^", + "@radix-ui/react-accordion": "^1.2.12", + "@tabler/icons-react": "^3.36.0", + "@tanstack/react-query": "^5.74.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "diff": "^8.0.2", + "fuse.js": "^7.1.0", + "heic-to": "^1.0.2", + "immer": "^11.1.0", + "katex": "^0.17.0", + "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-markdown": "^10.1.0", + "react-scroll-to-bottom": "^4.2.0", + "react-syntax-highlighter": "^16.1.0", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "shadcn": "^3.6.2", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "tw-animate-css": "^1.4.0", + "zustand": "^5" + } +} diff --git a/apps/vscode/resources/kimi-icon-storefront.png b/apps/vscode/resources/kimi-icon-storefront.png new file mode 100644 index 0000000000000000000000000000000000000000..033c0f4fb2e28a9daeca0205812d15bf3e547893 Binary files /dev/null and b/apps/vscode/resources/kimi-icon-storefront.png differ diff --git a/apps/vscode/resources/kimi-icon.svg b/apps/vscode/resources/kimi-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..db4631b8e427ef5fc84c29624d2de4be84bfd283 --- /dev/null +++ b/apps/vscode/resources/kimi-icon.svg @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/apps/vscode/scripts/extension-host-smoke.d.mts b/apps/vscode/scripts/extension-host-smoke.d.mts new file mode 100644 index 0000000000000000000000000000000000000000..eb723df5b065ff254da23e57e03a2f87cbe1aa8e --- /dev/null +++ b/apps/vscode/scripts/extension-host-smoke.d.mts @@ -0,0 +1,16 @@ +export interface ExtensionHostSmokeOptions { + version?: string; + vsixPath?: string; + cachePath?: string; +} + +export interface ExtensionHostSmokeResult { + version: string; + vscodeVersion: string; + vsixPath: string; + cachePath: string; +} + +export function runExtensionHostSmoke( + options?: ExtensionHostSmokeOptions, +): Promise; diff --git a/apps/vscode/scripts/local-cli.mjs b/apps/vscode/scripts/local-cli.mjs new file mode 100644 index 0000000000000000000000000000000000000000..ccc29ae7e30a072dc20d6a3b5d100b26858a97df --- /dev/null +++ b/apps/vscode/scripts/local-cli.mjs @@ -0,0 +1,47 @@ +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +export function resolveLocalCli(packageName, executableName) { + let packageJsonPath; + try { + packageJsonPath = require.resolve(`${packageName}/package.json`); + } catch (error) { + throw new Error( + `Local CLI dependency ${packageName} is not installed. Run pnpm install; runtime CLI downloads are disabled.`, + { cause: error }, + ); + } + + const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + const relativeBin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[executableName]; + if (typeof relativeBin !== 'string') { + throw new Error(`${packageName} does not declare the expected "${executableName}" binary.`); + } + return resolve(dirname(packageJsonPath), relativeBin); +} + +export function runLocalCli(packageName, executableName, args, options = {}) { + const cliPath = resolveLocalCli(packageName, executableName); + const result = spawnSync(process.execPath, [cliPath, ...args], { + cwd: options.cwd, + env: options.env ?? process.env, + encoding: options.encoding, + stdio: options.stdio ?? 'inherit', + }); + if (result.error !== undefined) { + throw new Error(`Unable to start local ${executableName}: ${result.error.message}`, { + cause: result.error, + }); + } + if (result.status !== 0) { + const output = [result.stdout, result.stderr].filter(Boolean).join(''); + throw new Error( + `Local ${executableName} exited with code ${result.status ?? 'unknown'}${output ? `:\n${output}` : ''}`, + ); + } + return result; +} diff --git a/apps/vscode/scripts/ovsx-publish.mjs b/apps/vscode/scripts/ovsx-publish.mjs new file mode 100644 index 0000000000000000000000000000000000000000..05809c6bcc0eeb31d6c34e0583b62d7838963daa --- /dev/null +++ b/apps/vscode/scripts/ovsx-publish.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +import { existsSync } from 'node:fs'; + +import { runLocalCli } from './local-cli.mjs'; +import { parsePublishArguments, publishUsage } from './publish-args.mjs'; +import { extensionRoot, isMainModule } from './vsix-targets.mjs'; +import { verifyVsix } from './vsix-verify.mjs'; + +async function main() { + const options = parsePublishArguments(process.argv.slice(2)); + if (options.help) { + console.log(publishUsage('Open VSX')); + return; + } + if (!process.env.OVSX_PAT) throw new Error('OVSX_PAT is required to publish.'); + + await verifyInputs(options); + for (const file of options.files) { + console.log(`Publishing verified package ${file}...`); + try { + runLocalCli('ovsx', 'ovsx', ['publish', file], { + cwd: extensionRoot, + encoding: 'utf8', + stdio: 'pipe', + }); + } catch (error) { + if (/already exists/i.test(error instanceof Error ? error.message : String(error))) { + console.log(`Package already exists: ${file}`); + continue; + } + throw error; + } + } +} + +async function verifyInputs(options) { + for (let index = 0; index < options.targets.length; index += 1) { + const file = options.files[index]; + if (!existsSync(file)) { + throw new Error(`Missing VSIX ${file}. Run pnpm run package:platform first.`); + } + await verifyVsix(file, options.targets[index], { sourceRoot: extensionRoot }); + } +} + +if (isMainModule(import.meta.url)) { + main().catch((error) => { + console.error(`Open VSX publish failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + }); +} diff --git a/apps/vscode/scripts/prepare-dev.mjs b/apps/vscode/scripts/prepare-dev.mjs new file mode 100644 index 0000000000000000000000000000000000000000..81f9fb8a966f6db2d5bb4c8a5173e94b063159d0 --- /dev/null +++ b/apps/vscode/scripts/prepare-dev.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { basename, dirname, join, parse, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { isMainModule } from './vsix-targets.mjs'; + +const SAFE_DIRECTORY_NAME = 'vscode-extension-dev'; +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const monorepoRoot = resolve(scriptDir, '../../..'); +const defaultBaseDir = join(monorepoRoot, '.tmp', SAFE_DIRECTORY_NAME); + +export async function prepareDevEnvironment(baseDir = defaultBaseDir) { + const root = resolve(baseDir); + assertSafeRoot(root); + await rm(root, { recursive: true, force: true }); + + const paths = { + root, + userData: join(root, 'user-data'), + extensions: join(root, 'extensions'), + kimiHome: join(root, 'kimi-home'), + workspace: join(root, 'workspace'), + }; + await Promise.all( + Object.values(paths) + .filter((path) => path !== root) + .map((path) => mkdir(path, { recursive: true })), + ); + await writeFile( + join(paths.workspace, 'README.md'), + '# Isolated Kimi Code extension development workspace\n', + ); + return paths; +} + +function assertSafeRoot(root) { + const parsed = parse(root); + if (root === parsed.root || basename(root) !== SAFE_DIRECTORY_NAME) { + throw new Error( + `Refusing to reset unsafe development directory "${root}"; it must end in ${SAFE_DIRECTORY_NAME}.`, + ); + } +} + +function parseArguments(argv) { + let baseDir = defaultBaseDir; + let help = false; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === '--') { + continue; + } else if (argument === '--help' || argument === '-h') { + help = true; + } else if (argument === '--base-dir') { + const value = argv[++index]; + if (value === undefined || value.startsWith('-')) throw new Error('--base-dir requires a value.'); + baseDir = value; + } else { + throw new Error(`Unknown option: ${argument}`); + } + } + return { baseDir, help }; +} + +async function main() { + const options = parseArguments(process.argv.slice(2)); + if (options.help) { + console.log('Usage: node scripts/prepare-dev.mjs [--base-dir <.../vscode-extension-dev>]'); + return; + } + const paths = await prepareDevEnvironment(options.baseDir); + console.log(`Prepared isolated VS Code profile: ${paths.root}`); + console.log(`KIMI_CODE_HOME=${paths.kimiHome}`); + console.log(`Workspace=${paths.workspace}`); +} + +if (isMainModule(import.meta.url)) { + main().catch((error) => { + console.error(`Development environment setup failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + }); +} diff --git a/apps/vscode/scripts/publish-args.mjs b/apps/vscode/scripts/publish-args.mjs new file mode 100644 index 0000000000000000000000000000000000000000..ce2f095c25e523c3d8560322ca2ec82e78e4743f --- /dev/null +++ b/apps/vscode/scripts/publish-args.mjs @@ -0,0 +1,54 @@ +import { join, resolve } from 'node:path'; + +import { + defaultVsixOutputDir, + normalizeVsixTargets, + vsixFileName, +} from './vsix-targets.mjs'; + +export function parsePublishArguments(argv) { + const targets = []; + let outputDir = defaultVsixOutputDir; + let help = false; + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === '--') { + continue; + } else if (argument === '--help' || argument === '-h') { + help = true; + } else if (argument === '--out-dir') { + outputDir = requireOptionValue(argv, ++index, '--out-dir'); + } else if (argument === '--target') { + targets.push(requireOptionValue(argv, ++index, '--target')); + } else if (argument.startsWith('-')) { + throw new Error(`Unknown option: ${argument}`); + } else { + targets.push(argument); + } + } + + const normalizedTargets = normalizeVsixTargets(targets); + const resolvedOutputDir = resolve(outputDir); + return { + help, + targets: normalizedTargets, + outputDir: resolvedOutputDir, + files: normalizedTargets.map((target) => join(resolvedOutputDir, vsixFileName(target))), + }; +} + +function requireOptionValue(argv, index, option) { + const value = argv[index]; + if (value !== undefined && !value.startsWith('-')) return value; + throw new Error(`${option} requires a value.`); +} + +export function publishUsage(marketplace) { + return [ + `Usage: node scripts/${marketplace === 'Open VSX' ? 'ovsx' : 'vsix'}-publish.mjs [targets...] [--out-dir ]`, + '', + `Publishes already-built, re-verified VSIX files to ${marketplace}.`, + 'This command never builds or downloads a CLI.', + ].join('\n'); +} diff --git a/apps/vscode/scripts/vsix-package.mjs b/apps/vscode/scripts/vsix-package.mjs new file mode 100644 index 0000000000000000000000000000000000000000..0558e1eb82a307d5f09ff6f614ac98adeec90adb --- /dev/null +++ b/apps/vscode/scripts/vsix-package.mjs @@ -0,0 +1,141 @@ +#!/usr/bin/env node +import { createRequire } from 'node:module'; +import { mkdir } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; + +import { runLocalCli } from './local-cli.mjs'; +import { + defaultVsixOutputDir, + extensionRoot, + isMainModule, + normalizeVsixTargets, + vsixFileName, +} from './vsix-targets.mjs'; +import { verifyVsix } from './vsix-verify.mjs'; + +const require = createRequire(import.meta.url); + +function parseArguments(argv) { + const targets = []; + let outputDir = defaultVsixOutputDir; + let dryRun = false; + let help = false; + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === '--') { + continue; + } else if (argument === '--help' || argument === '-h') { + help = true; + } else if (argument === '--dry-run') { + dryRun = true; + } else if (argument === '--out-dir') { + outputDir = requireOptionValue(argv, ++index, '--out-dir'); + } else if (argument === '--target') { + targets.push(requireOptionValue(argv, ++index, '--target')); + } else if (argument.startsWith('-')) { + throw new Error(`Unknown option: ${argument}`); + } else { + targets.push(argument); + } + } + + return { + targets: normalizeVsixTargets(targets), + outputDir: resolve(outputDir), + dryRun, + help, + }; +} + +function requireOptionValue(argv, index, option) { + const value = argv[index]; + if (value !== undefined && !value.startsWith('-')) return value; + throw new Error(`${option} requires a value.`); +} + +function usage() { + return [ + 'Usage: node scripts/vsix-package.mjs [targets...] [--out-dir ]', + ' node scripts/vsix-package.mjs --target win32-x64 --dry-run', + '', + 'With no targets, all six supported VSIX targets are built and audited.', + 'This command never publishes an extension.', + ].join('\n'); +} + +async function main() { + const options = parseArguments(process.argv.slice(2)); + if (options.help) { + console.log(usage()); + return; + } + + await mkdir(options.outputDir, { recursive: true }); + console.log(`VSIX targets: ${options.targets.join(', ')}`); + console.log(`Output directory: ${options.outputDir}`); + + if (!options.dryRun) { + console.log('Building the extension once before packaging all targets...'); + buildExtension(); + } + + const packVsix = options.dryRun ? undefined : loadVscePack(); + + for (const target of options.targets) { + const outputPath = join(options.outputDir, vsixFileName(target)); + if (options.dryRun) { + console.log(`Would package ${target} -> ${outputPath}`); + continue; + } + + console.log(`\nPackaging ${target} with the workspace @vscode/vsce...`); + await packVsix({ + cwd: extensionRoot, + dependencies: false, + packagePath: outputPath, + target, + }); + const result = await verifyVsix(outputPath, target, { sourceRoot: extensionRoot }); + console.log( + `Verified ${target}: ${result.files} files, ${result.bytes} unpacked bytes; package/static checks passed.`, + ); + } + + if (!options.dryRun) { + console.log( + '\nVSIX packaging complete. These are package-only results until each target runs in its matching extension host.', + ); + } +} + +function buildExtension() { + runLocalCli('tsdown', 'tsdown', ['--config', 'tsdown.config.ts'], { cwd: extensionRoot }); + runLocalCli('vite', 'vite', ['build', '--config', 'webview-ui/vite.config.ts'], { + cwd: extensionRoot, + }); +} + +function loadVscePack() { + try { + // VSCE's public API always runs vscode:prepublish. We build once above and pin VSCE, + // so use its package phase directly instead of rebuilding for every target. + const module = require('@vscode/vsce/out/package.js'); + if (typeof module.pack !== 'function') { + throw new TypeError('the installed package does not expose pack()'); + } + return module.pack; + } catch (error) { + throw new Error( + 'The workspace @vscode/vsce package API is unavailable. Run pnpm install; runtime CLI downloads are disabled.', + { cause: error }, + ); + } +} + +if (isMainModule(import.meta.url)) { + main().catch((error) => { + console.error(`VSIX packaging failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + }); +} diff --git a/apps/vscode/scripts/vsix-publish.mjs b/apps/vscode/scripts/vsix-publish.mjs new file mode 100644 index 0000000000000000000000000000000000000000..e7952478604d5dbc2f52e278d12871653ef260d6 --- /dev/null +++ b/apps/vscode/scripts/vsix-publish.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import { existsSync } from 'node:fs'; + +import { runLocalCli } from './local-cli.mjs'; +import { parsePublishArguments, publishUsage } from './publish-args.mjs'; +import { extensionRoot, isMainModule } from './vsix-targets.mjs'; +import { verifyVsix } from './vsix-verify.mjs'; + +async function main() { + const options = parsePublishArguments(process.argv.slice(2)); + if (options.help) { + console.log(publishUsage('Visual Studio Marketplace')); + return; + } + if (!process.env.VSCE_PAT) throw new Error('VSCE_PAT is required to publish.'); + + await verifyInputs(options); + for (const file of options.files) { + console.log(`Publishing verified package ${file}...`); + runLocalCli( + '@vscode/vsce', + 'vsce', + ['publish', '--packagePath', file, '--skip-duplicate'], + { cwd: extensionRoot }, + ); + } +} + +async function verifyInputs(options) { + for (let index = 0; index < options.targets.length; index += 1) { + const file = options.files[index]; + if (!existsSync(file)) { + throw new Error(`Missing VSIX ${file}. Run pnpm run package:platform first.`); + } + await verifyVsix(file, options.targets[index], { sourceRoot: extensionRoot }); + } +} + +if (isMainModule(import.meta.url)) { + main().catch((error) => { + console.error(`Marketplace publish failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + }); +} diff --git a/apps/vscode/scripts/vsix-targets.mjs b/apps/vscode/scripts/vsix-targets.mjs new file mode 100644 index 0000000000000000000000000000000000000000..b3314ad419545aa87a7e70baed9a32d521dd9738 --- /dev/null +++ b/apps/vscode/scripts/vsix-targets.mjs @@ -0,0 +1,45 @@ +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +export const VSIX_TARGETS = Object.freeze([ + 'darwin-x64', + 'darwin-arm64', + 'linux-x64', + 'linux-arm64', + 'win32-x64', + 'win32-arm64', +]); + +export const extensionRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +export const defaultVsixOutputDir = join(extensionRoot, 'artifacts', 'vsix'); + +export function vsixFileName(target) { + assertVsixTarget(target); + return `kimi-code-${target}.vsix`; +} + +export function normalizeVsixTargets(values) { + const requested = values.length === 0 || values.includes('all') ? VSIX_TARGETS : values; + if (values.includes('all') && values.length > 1) { + throw new Error('Target "all" cannot be combined with an explicit VSIX target.'); + } + + const targets = []; + for (const target of requested) { + assertVsixTarget(target); + if (!targets.includes(target)) targets.push(target); + } + return targets; +} + +export function assertVsixTarget(target) { + if (VSIX_TARGETS.includes(target)) return; + throw new Error( + `Unknown VSIX target "${target}". Expected one of: ${VSIX_TARGETS.join(', ')}`, + ); +} + +export function isMainModule(metaUrl) { + const argvPath = process.argv[1]; + return argvPath !== undefined && pathToFileURL(resolve(argvPath)).href === metaUrl; +} diff --git a/apps/vscode/scripts/vsix-verify.mjs b/apps/vscode/scripts/vsix-verify.mjs new file mode 100644 index 0000000000000000000000000000000000000000..ee81434cc602d7b0e850c9e3af79c89124d37bbe --- /dev/null +++ b/apps/vscode/scripts/vsix-verify.mjs @@ -0,0 +1,490 @@ +#!/usr/bin/env node +import { builtinModules } from 'node:module'; +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { + mkdtemp, + mkdir, + readFile, + readdir, + rm, + stat, + writeFile, +} from 'node:fs/promises'; +import { homedir, tmpdir } from 'node:os'; +import { dirname, extname, join, relative, resolve, sep } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { isDeepStrictEqual } from 'node:util'; +import { parse } from 'acorn'; + +import { extractZip } from './zip.mjs'; +import { + defaultVsixOutputDir, + extensionRoot, + isMainModule, + normalizeVsixTargets, + vsixFileName, +} from './vsix-targets.mjs'; + +const REQUIRED_WEBVIEW_FILES = [ + 'dist/webview.js', + 'dist/kimi-banner-dark.svg', + 'dist/kimi-banner-light.svg', + 'dist/kimi-logo.png', +]; +const FORBIDDEN_PATH_SEGMENTS = new Set([ + '.kimi', + '.kimi-code', + '.vscode', + '__tests__', + 'cache', + 'caches', + 'credentials', + 'logs', + 'node_modules', + 'profile', + 'profiles', + 'runtime', + 'scripts', + 'session', + 'sessions', + 'src', + 'state', + 'states', + 'test', + 'tests', + 'tokens', + 'webview-ui', +]); +const FORBIDDEN_EXTENSIONS = new Set(['.jsonl', '.log', '.map', '.py', '.pyc', '.ts', '.tsx']); +const TEXT_EXTENSIONS = new Set([ + '.cjs', + '.css', + '.html', + '.js', + '.json', + '.md', + '.mjs', + '.svg', + '.txt', + '.xml', +]); +const BUILTIN_IMPORTS = new Set( + builtinModules.flatMap((name) => [name, name.startsWith('node:') ? name.slice(5) : `node:${name}`]), +); +// `ws` probes these native accelerators inside try/catch and immediately uses +// its bundled JavaScript fallback when they are absent. They are not required +// runtime dependencies and must not be shipped as cross-platform native code. +const OPTIONAL_FALLBACK_IMPORTS = new Set(['bufferutil', 'canvas', 'utf-8-validate']); +const MANIFEST_FIELDS = [ + 'name', + 'publisher', + 'displayName', + 'version', + 'engines', + 'extensionKind', + 'capabilities', + 'activationEvents', + 'main', + 'icon', +]; +const CONTRIBUTE_FIELDS = [ + 'commands', + 'configuration', + 'keybindings', + 'menus', + 'views', + 'viewsContainers', +]; + +export async function verifyVsix(vsixPath, target, options = {}) { + const extractionRoot = await mkdtemp(join(tmpdir(), 'kimi-vsix-audit-')); + try { + await extractZip(vsixPath, extractionRoot); + return await auditExtractedVsix(extractionRoot, target, options); + } finally { + await rm(extractionRoot, { recursive: true, force: true }); + } +} + +export async function auditExtractedVsix(extractionRoot, target, options = {}) { + const sourceRoot = options.sourceRoot ?? extensionRoot; + const extensionDir = join(extractionRoot, 'extension'); + const files = await listFiles(extractionRoot); + const fileSet = new Set(files); + + requireFile(fileSet, 'extension.vsixmanifest'); + requireFile(fileSet, '[Content_Types].xml'); + requireFile(fileSet, 'extension/package.json'); + const packagedManifest = await readJson(join(extensionDir, 'package.json'), 'package.json'); + const sourceManifest = await readJson(join(sourceRoot, 'package.json'), 'source package.json'); + + await verifyTargetManifest(extractionRoot, target); + verifyPackageManifest(packagedManifest, sourceManifest); + verifyRequiredFiles(fileSet, packagedManifest); + verifyForbiddenFiles(files); + await verifyNoSensitiveContent(extractionRoot, files, sourceRoot, options.forbiddenText ?? []); + await verifyRuntimeImports(extensionDir, files); + await verifyEntryImport(extensionDir, packagedManifest.main); + + const bytes = await totalSize(extractionRoot, files); + return { target, files: files.length, bytes }; +} + +async function verifyTargetManifest(extractionRoot, target) { + const xml = await readFile(join(extractionRoot, 'extension.vsixmanifest'), 'utf8'); + const actual = xml.match(/\bTargetPlatform="([^"]+)"/)?.[1]; + if (actual !== target) { + throw new Error( + `VSIX manifest target is ${actual ?? 'missing'}, expected ${target}.`, + ); + } +} + +function verifyPackageManifest(packaged, source) { + if (typeof packaged.main !== 'string' || !packaged.main.endsWith('.js')) { + throw new Error(`Packaged extension main must be a .js entry, got ${String(packaged.main)}.`); + } + if (packaged.main !== './dist/extension.js') { + throw new Error(`Packaged extension main is ${packaged.main}, expected ./dist/extension.js.`); + } + + for (const field of MANIFEST_FIELDS) { + if (!isDeepStrictEqual(packaged[field], source[field])) { + throw new Error(`Packaged package.json field "${field}" does not match the source manifest.`); + } + } + for (const field of CONTRIBUTE_FIELDS) { + if (!isDeepStrictEqual(packaged.contributes?.[field], source.contributes?.[field])) { + throw new Error( + `Packaged package.json contributes.${field} does not match the source manifest.`, + ); + } + } +} + +function verifyRequiredFiles(fileSet, manifest) { + const required = [ + 'extension/LICENSE.txt', + `extension/${stripLeadingDotSlash(manifest.main)}`, + ...REQUIRED_WEBVIEW_FILES.map((file) => `extension/${file}`), + ]; + requireOneOf(fileSet, ['extension/README.md', 'extension/readme.md'], 'marketplace README'); + if (typeof manifest.icon === 'string') required.push(`extension/${manifest.icon}`); + + for (const container of Object.values(manifest.contributes?.viewsContainers ?? {})) { + if (!Array.isArray(container)) continue; + for (const view of container) { + if (typeof view?.icon === 'string') required.push(`extension/${view.icon}`); + } + } + for (const file of required) requireFile(fileSet, file); +} + +function verifyForbiddenFiles(files) { + for (const file of files) { + const normalized = file.replaceAll('\\', '/'); + const lower = normalized.toLowerCase(); + const extensionRelative = lower.startsWith('extension/') ? lower.slice('extension/'.length) : lower; + const segments = extensionRelative.split('/'); + const forbiddenSegment = segments.find((segment) => FORBIDDEN_PATH_SEGMENTS.has(segment)); + if (forbiddenSegment !== undefined) { + throw new Error(`Forbidden package path segment "${forbiddenSegment}" in ${normalized}.`); + } + if (FORBIDDEN_EXTENSIONS.has(extname(lower))) { + throw new Error(`Forbidden package file type in ${normalized}.`); + } + if ( + lower.includes('kimi-agent-sdk') || + lower.includes('download-cli') || + lower.includes('/bin/kimi/') || + /(^|\/)uv(?:\.exe)?$/.test(lower) + ) { + throw new Error(`Legacy CLI/runtime artifact found in ${normalized}.`); + } + } +} + +async function verifyNoSensitiveContent(extractionRoot, files, sourceRoot, extraForbiddenText) { + const secretValues = [process.env.VSCE_PAT, process.env.OVSX_PAT] + .filter((value) => typeof value === 'string' && value.length >= 8); + const forbidden = [sourceRoot, homedir(), ...extraForbiddenText, ...secretValues] + .filter((value) => typeof value === 'string' && value.length >= 4) + .flatMap((value) => [value, value.replaceAll('\\', '/'), value.replaceAll('/', '\\')]); + + for (const file of files) { + if (!TEXT_EXTENSIONS.has(extname(file).toLowerCase())) continue; + const content = await readFile(join(extractionRoot, file), 'utf8'); + const match = forbidden.find((value) => content.includes(value)); + if (match === undefined) continue; + const label = secretValues.includes(match) ? 'a marketplace token' : 'a local filesystem path'; + throw new Error(`Packaged text file ${file} contains ${label}.`); + } +} + +async function verifyRuntimeImports(extensionDir, files) { + const distFiles = files.filter( + (file) => file.startsWith('extension/dist/') && ['.cjs', '.js', '.mjs'].includes(extname(file)), + ); + if (distFiles.length === 0) throw new Error('No JavaScript extension bundle files were packaged.'); + + for (const archivePath of distFiles) { + const localPath = join(dirname(extensionDir), archivePath); + const source = await readFile(localPath, 'utf8'); + for (const specifier of collectLiteralImports(source)) { + if (specifier === 'vscode' || BUILTIN_IMPORTS.has(specifier)) continue; + if (OPTIONAL_FALLBACK_IMPORTS.has(specifier)) continue; + if (specifier.startsWith('node:')) continue; + if (specifier.startsWith('.') || specifier.startsWith('/')) { + if (specifier.startsWith('/')) { + throw new Error(`Absolute runtime import "${specifier}" in ${archivePath}.`); + } + const dependencyPath = resolve(dirname(localPath), stripImportSuffix(specifier)); + if (!runtimeImportExists(dependencyPath)) { + throw new Error(`Missing relative runtime import "${specifier}" in ${archivePath}.`); + } + continue; + } + if (specifier.startsWith('data:') || specifier.startsWith('file:')) continue; + throw new Error(`Bare runtime dependency "${specifier}" remains in ${archivePath}.`); + } + } +} + +function collectLiteralImports(source) { + const imports = new Set(); + const program = parse(source, { + allowHashBang: true, + ecmaVersion: 'latest', + sourceType: 'module', + }); + walkSyntax(program, (node) => { + if ( + node.type === 'ImportDeclaration' || + node.type === 'ExportAllDeclaration' || + node.type === 'ExportNamedDeclaration' + ) { + const specifier = literalString(node.source); + if (specifier !== undefined) imports.add(specifier); + return; + } + if (node.type === 'ImportExpression') { + const specifier = literalString(node.source); + if (specifier !== undefined) imports.add(specifier); + return; + } + if (node.type === 'CallExpression' && isRuntimeRequire(node.callee)) { + const specifier = literalString(node.arguments?.[0]); + if (specifier !== undefined) imports.add(specifier); + } + }); + return imports; +} + +function walkSyntax(value, visit) { + if (Array.isArray(value)) { + for (const item of value) walkSyntax(item, visit); + return; + } + if (typeof value !== 'object' || value === null) return; + if (typeof value.type === 'string') visit(value); + for (const [key, child] of Object.entries(value)) { + if (key === 'start' || key === 'end' || key === 'loc' || key === 'range') continue; + walkSyntax(child, visit); + } +} + +function isRuntimeRequire(callee) { + if (callee?.type === 'Identifier') return /^(?:__)?require\d*$/.test(callee.name); + if (callee?.type !== 'MemberExpression' || callee.computed === true) return false; + // `this.require(...)` is an ordinary class method call (e.g. a private field + // accessor in bundled sources), never a CommonJS require of a bare specifier. + if (callee.object?.type === 'ThisExpression') return false; + return callee.property?.type === 'Identifier' && callee.property.name === 'require'; +} + +function literalString(node) { + if (node?.type === 'Literal' && typeof node.value === 'string') return node.value; + if (node?.type === 'TemplateLiteral' && node.expressions?.length === 0) { + return node.quasis?.[0]?.value?.cooked; + } + return undefined; +} + +async function verifyEntryImport(extensionDir, main) { + const mainPath = join(extensionDir, stripLeadingDotSlash(main)); + const stubDir = join(extensionDir, 'node_modules', 'vscode'); + await mkdir(stubDir, { recursive: true }); + await writeFile( + join(stubDir, 'package.json'), + `${JSON.stringify({ name: 'vscode', version: '0.0.0-test', type: 'module', exports: './index.js' }, null, 2)}\n`, + ); + await writeFile(join(stubDir, 'index.js'), 'export {};\n'); + + const script = [ + `const extension = await import(${JSON.stringify(pathToFileURL(mainPath).href)});`, + 'if (typeof extension.activate !== "function") {', + ' throw new Error("extension bundle does not export activate");', + '}', + ].join('\n'); + const env = { ...process.env }; + delete env.NODE_PATH; + const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], { + cwd: dirname(extensionDir), + env, + encoding: 'utf8', + timeout: 30_000, + }); + await rm(join(extensionDir, 'node_modules'), { recursive: true, force: true }); + if (result.error !== undefined) { + throw new Error(`Unable to import the unpacked extension entry: ${result.error.message}`); + } + if (result.status !== 0) { + const detail = conciseProcessError(result.stderr || result.stdout); + throw new Error(`Unpacked extension entry import failed: ${detail}`); + } +} + +async function readJson(path, label) { + try { + return JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + throw new Error(`${label} is not valid JSON: ${describeError(error)}`, { cause: error }); + } +} + +async function listFiles(root) { + const output = []; + async function visit(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const localPath = join(directory, entry.name); + if (entry.isDirectory()) { + await visit(localPath); + } else if (entry.isFile()) { + output.push(relative(root, localPath).split(sep).join('/')); + } else { + throw new Error(`Unsupported non-file entry in unpacked VSIX: ${localPath}`); + } + } + } + await visit(root); + return output.sort(); +} + +async function totalSize(root, files) { + let bytes = 0; + for (const file of files) bytes += (await stat(join(root, file))).size; + return bytes; +} + +function requireFile(fileSet, file) { + if (fileSet.has(file)) return; + throw new Error(`Required VSIX resource is missing: ${file}`); +} + +function requireOneOf(fileSet, files, label) { + if (files.some((file) => fileSet.has(file))) return; + throw new Error(`Required VSIX resource is missing: ${label} (${files.join(' or ')}).`); +} + +function runtimeImportExists(path) { + return [path, `${path}.js`, `${path}.mjs`, `${path}.cjs`, join(path, 'index.js')].some(existsSync); +} + +function stripImportSuffix(specifier) { + return specifier.split(/[?#]/, 1)[0]; +} + +function stripLeadingDotSlash(value) { + return String(value).replace(/^\.\//, ''); +} + +function describeError(error) { + return error instanceof Error ? error.message : String(error); +} + +function conciseProcessError(output) { + const lines = String(output).trim().split(/\r?\n/).filter(Boolean); + return lines.slice(-4).join('\n') || 'process exited without an error message'; +} + +function parseArguments(argv) { + const targets = []; + let outputDir = defaultVsixOutputDir; + let file; + let directory; + let help = false; + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === '--') { + continue; + } else if (argument === '--help' || argument === '-h') { + help = true; + } else if (argument === '--out-dir') { + outputDir = requireOptionValue(argv, ++index, '--out-dir'); + } else if (argument === '--target') { + targets.push(requireOptionValue(argv, ++index, '--target')); + } else if (argument === '--file') { + file = requireOptionValue(argv, ++index, '--file'); + } else if (argument === '--directory') { + directory = requireOptionValue(argv, ++index, '--directory'); + } else if (argument.startsWith('-')) { + throw new Error(`Unknown option: ${argument}`); + } else { + targets.push(argument); + } + } + + if (file !== undefined && directory !== undefined) { + throw new Error('--file and --directory cannot be used together.'); + } + const normalizedTargets = normalizeVsixTargets(targets); + if ((file !== undefined || directory !== undefined) && normalizedTargets.length !== 1) { + throw new Error('--file and --directory require exactly one target.'); + } + return { targets: normalizedTargets, outputDir: resolve(outputDir), file, directory, help }; +} + +function requireOptionValue(argv, index, option) { + const value = argv[index]; + if (value !== undefined && !value.startsWith('-')) return value; + throw new Error(`${option} requires a value.`); +} + +function usage() { + return [ + 'Usage: node scripts/vsix-verify.mjs [targets...] [--out-dir ]', + ' node scripts/vsix-verify.mjs --target --file ', + ' node scripts/vsix-verify.mjs --target --directory ', + '', + 'The verifier performs a package-content audit and an entry import smoke only.', + 'It does not claim that a target passed a real operating-system E2E run.', + ].join('\n'); +} + +async function main() { + const options = parseArguments(process.argv.slice(2)); + if (options.help) { + console.log(usage()); + return; + } + + for (const target of options.targets) { + const input = options.directory ?? options.file ?? join(options.outputDir, vsixFileName(target)); + const result = options.directory === undefined + ? await verifyVsix(resolve(input), target, { sourceRoot: extensionRoot }) + : await auditExtractedVsix(resolve(input), target, { sourceRoot: extensionRoot }); + console.log( + `Verified ${target}: ${result.files} files, ${result.bytes} unpacked bytes; static audit and entry import smoke passed (package-only).`, + ); + } +} + +if (isMainModule(import.meta.url)) { + main().catch((error) => { + console.error(`VSIX verification failed: ${describeError(error)}`); + process.exitCode = 1; + }); +} diff --git a/apps/vscode/scripts/watch-extension.mjs b/apps/vscode/scripts/watch-extension.mjs new file mode 100644 index 0000000000000000000000000000000000000000..9dadf992d06e479842ae3b25fe2beaf550d5bc35 --- /dev/null +++ b/apps/vscode/scripts/watch-extension.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +import { existsSync } from 'node:fs'; +import { readdir, stat } from 'node:fs/promises'; +import { extname, join, resolve } from 'node:path'; + +import { runLocalCli } from './local-cli.mjs'; +import { extensionRoot, isMainModule } from './vsix-targets.mjs'; + +const sourceDirectories = [ + join(extensionRoot, 'src'), + join(extensionRoot, 'shared'), + ...[ + 'kaos', + 'kosong', + 'migration-legacy', + 'node-sdk', + 'oauth', + 'protocol', + 'telemetry', + ].map((name) => resolve(extensionRoot, `../../packages/${name}/src`)), +].filter(existsSync); +const rootConfigFiles = ['package.json', 'tsconfig.json', 'tsdown.config.ts'].map((name) => + join(extensionRoot, name), +); +const watchedExtensions = new Set(['.cts', '.js', '.json', '.md', '.mjs', '.mts', '.ts', '.tsx']); +const pollIntervalMs = 1_500; + +function buildExtension() { + runLocalCli( + 'tsdown', + 'tsdown', + ['--config', join(extensionRoot, 'tsdown.config.ts'), '--sourcemap'], + { cwd: extensionRoot }, + ); +} + +async function main() { + buildExtension(); + let snapshot = await sourceSnapshot(); + let checking = false; + const poll = async () => { + if (checking) return; + checking = true; + try { + const nextSnapshot = await sourceSnapshot(); + if (nextSnapshot === snapshot) return; + snapshot = nextSnapshot; + console.log('\nExtension source changed; rebuilding with sourcemaps...'); + buildExtension(); + console.log('Extension rebuild complete.'); + } catch (error) { + console.error(`Extension watch check failed: ${error instanceof Error ? error.message : String(error)}`); + } finally { + checking = false; + } + }; + const interval = setInterval(() => void poll(), pollIntervalMs); + + const close = () => { + clearInterval(interval); + process.exit(0); + }; + process.once('SIGINT', close); + process.once('SIGTERM', close); + console.log('Extension dev build complete; watching for changes.'); +} + +async function sourceSnapshot() { + const records = []; + for (const directory of sourceDirectories) await collectSourceRecords(directory, records); + for (const file of rootConfigFiles) await addFileRecord(file, records); + return records.sort().join('\n'); +} + +async function collectSourceRecords(directory, records) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await collectSourceRecords(path, records); + } else if (entry.isFile() && watchedExtensions.has(extname(entry.name))) { + await addFileRecord(path, records); + } + } +} + +async function addFileRecord(path, records) { + const info = await stat(path); + records.push(`${path}:${info.size}:${info.mtimeMs}`); +} + +if (isMainModule(import.meta.url)) { + main().catch((error) => { + console.error(`Extension watch failed: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + }); +} diff --git a/apps/vscode/scripts/zip.mjs b/apps/vscode/scripts/zip.mjs new file mode 100644 index 0000000000000000000000000000000000000000..27df84d44ac404f296e429aab203ade3800efc6c --- /dev/null +++ b/apps/vscode/scripts/zip.mjs @@ -0,0 +1,122 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, resolve, sep } from 'node:path'; +import { inflateRawSync } from 'node:zlib'; + +const END_OF_CENTRAL_DIRECTORY = 0x06054b50; +const CENTRAL_DIRECTORY_ENTRY = 0x02014b50; +const LOCAL_FILE_HEADER = 0x04034b50; +const MAX_END_RECORD_SEARCH = 65_535 + 22; + +export async function extractZip(archivePath, destination) { + const archive = await readFile(archivePath); + const entries = readCentralDirectory(archive); + + for (const entry of entries) { + const relativePath = safeArchivePath(entry.name); + if (relativePath === '') continue; + const outputPath = resolve(destination, relativePath); + assertInside(destination, outputPath, entry.name); + + if (entry.name.endsWith('/')) { + await mkdir(outputPath, { recursive: true }); + continue; + } + + const content = readEntry(archive, entry); + await mkdir(dirname(outputPath), { recursive: true }); + await writeFile(outputPath, content); + } + + return entries.map((entry) => entry.name); +} + +function readCentralDirectory(archive) { + const endOffset = findEndRecord(archive); + const entryCount = archive.readUInt16LE(endOffset + 10); + const directorySize = archive.readUInt32LE(endOffset + 12); + const directoryOffset = archive.readUInt32LE(endOffset + 16); + + if (entryCount === 0xffff || directorySize === 0xffffffff || directoryOffset === 0xffffffff) { + throw new Error('ZIP64 VSIX archives are not supported by the verifier.'); + } + if (directoryOffset + directorySize > archive.length) { + throw new Error('VSIX central directory points outside the archive.'); + } + + const entries = []; + let offset = directoryOffset; + for (let index = 0; index < entryCount; index += 1) { + if (archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_ENTRY) { + throw new Error(`Invalid VSIX central-directory entry at byte ${offset}.`); + } + const flags = archive.readUInt16LE(offset + 8); + if ((flags & 0x1) !== 0) throw new Error('Encrypted VSIX entries are not supported.'); + + const compression = archive.readUInt16LE(offset + 10); + const compressedSize = archive.readUInt32LE(offset + 20); + const uncompressedSize = archive.readUInt32LE(offset + 24); + const nameLength = archive.readUInt16LE(offset + 28); + const extraLength = archive.readUInt16LE(offset + 30); + const commentLength = archive.readUInt16LE(offset + 32); + const localOffset = archive.readUInt32LE(offset + 42); + const nameStart = offset + 46; + const name = archive.subarray(nameStart, nameStart + nameLength).toString('utf8'); + entries.push({ name, compression, compressedSize, uncompressedSize, localOffset }); + offset = nameStart + nameLength + extraLength + commentLength; + } + return entries; +} + +function findEndRecord(archive) { + const lowerBound = Math.max(0, archive.length - MAX_END_RECORD_SEARCH); + for (let offset = archive.length - 22; offset >= lowerBound; offset -= 1) { + if (archive.readUInt32LE(offset) === END_OF_CENTRAL_DIRECTORY) return offset; + } + throw new Error('VSIX is not a readable ZIP archive: end record is missing.'); +} + +function readEntry(archive, entry) { + if (archive.readUInt32LE(entry.localOffset) !== LOCAL_FILE_HEADER) { + throw new Error(`Invalid local header for VSIX entry ${entry.name}.`); + } + const nameLength = archive.readUInt16LE(entry.localOffset + 26); + const extraLength = archive.readUInt16LE(entry.localOffset + 28); + const dataStart = entry.localOffset + 30 + nameLength + extraLength; + const dataEnd = dataStart + entry.compressedSize; + if (dataEnd > archive.length) throw new Error(`Truncated VSIX entry ${entry.name}.`); + + const compressed = archive.subarray(dataStart, dataEnd); + let content; + if (entry.compression === 0) { + content = compressed; + } else if (entry.compression === 8) { + content = inflateRawSync(compressed); + } else { + throw new Error(`Unsupported compression method ${entry.compression} for ${entry.name}.`); + } + if (content.length !== entry.uncompressedSize) { + throw new Error( + `Unexpected uncompressed size for ${entry.name}: ${content.length}, expected ${entry.uncompressedSize}.`, + ); + } + return content; +} + +function safeArchivePath(name) { + const normalized = name.replaceAll('\\', '/'); + const segments = normalized.split('/').filter((segment) => segment !== ''); + if ( + normalized.startsWith('/') || + /^[A-Za-z]:/.test(normalized) || + segments.some((segment) => segment === '..') + ) { + throw new Error(`Unsafe path in VSIX archive: ${name}`); + } + return segments.join(sep); +} + +function assertInside(destination, outputPath, archiveName) { + const root = resolve(destination); + if (outputPath === root || outputPath.startsWith(`${root}${sep}`)) return; + throw new Error(`Unsafe path in VSIX archive: ${archiveName}`); +} diff --git a/apps/vscode/shared/bridge.ts b/apps/vscode/shared/bridge.ts new file mode 100644 index 0000000000000000000000000000000000000000..d7f9bd88ae9974e0763d2bc558891f73522f14d3 --- /dev/null +++ b/apps/vscode/shared/bridge.ts @@ -0,0 +1,325 @@ +/** + * Bridge Protocol - Communication between VS Code extension and webview. + * + * Architecture: + * - Webview calls Methods via RPC (request/response) + * - Extension broadcasts Events to webview (one-way notifications) + * + * RPC flow: webview.call(method, params) -> extension.dispatch -> webview.resolve(result) + * Event flow: extension.broadcast(event, data) -> webview.on(event, handler) + */ + +export const Methods = { + CheckWorkspace: "checkWorkspace", + GetInputHistory: "getInputHistory", + AddInputHistory: "addInputHistory", + + GetSlashCommands: "getSlashCommands", + CheckLoginStatus: "checkLoginStatus", + Login: "login", + Logout: "logout", + SaveConfig: "saveConfig", + GetExtensionConfig: "getExtensionConfig", + OpenSettings: "openSettings", + OpenFolder: "openFolder", + GetModels: "getModels", + + GetMCPServers: "getMCPServers", + AddMCPServer: "addMCPServer", + UpdateMCPServer: "updateMCPServer", + RemoveMCPServer: "removeMCPServer", + AuthMCP: "authMCP", + ResetAuthMCP: "resetAuthMCP", + TestMCP: "testMCP", + + StreamChat: "streamChat", + AbortChat: "abortChat", + ResetSession: "resetSession", + SetPlanMode: "setPlanMode", + SteerChat: "steerChat", + RespondApproval: "respondApproval", + + GetKimiSessions: "getKimiSessions", + GetAllKimiSessions: "getAllKimiSessions", + GetRegisteredWorkDirs: "getRegisteredWorkDirs", + SetWorkDir: "setWorkDir", + BrowseWorkDir: "browseWorkDir", + LoadKimiSessionHistory: "loadKimiSessionHistory", + DeleteKimiSession: "deleteKimiSession", + ForkKimiSession: "forkKimiSession", + GetProjectFiles: "getProjectFiles", + PickMedia: "pickMedia", + OpenFile: "openFile", + CheckFileExists: "checkFileExists", + CheckFilesExist: "checkFilesExist", + OpenFileDiff: "openFileDiff", + TrackFiles: "trackFiles", + ClearTrackedFiles: "clearTrackedFiles", + RevertFiles: "revertFiles", + KeepChanges: "keepChanges", + GetImageDataUri: "getImageDataUri", + ShowLogs: "showLogs", + ReloadWebview: "reloadWebview", + RespondQuestion: "respondQuestion", +} as const; + +export type RpcMethod = (typeof Methods)[keyof typeof Methods]; + +export interface RpcMessage { + readonly id: string; + readonly method: RpcMethod; + readonly params?: unknown; +} + +export interface RpcResult { + readonly id: string; + readonly result?: unknown; + readonly error?: string; +} + +export type RpcMessageValidation = + | { readonly ok: true; readonly message: RpcMessage } + | { + readonly ok: false; + readonly id: string; + readonly method: string; + readonly error: string; + }; + +export const Events = { + ExtensionConfigChanged: "extensionConfigChanged", + MCPServersChanged: "mcpServersChanged", + StreamEvent: "streamEvent", + FocusInput: "focusInput", + InsertMention: "insertMention", + NewConversation: "newConversation", + FileChangesUpdated: "fileChangesUpdated", + RollbackInput: "rollbackInput", + LoginUrl: "loginUrl", +} as const; + +const rpcMethods = new Set(Object.values(Methods)); + +/** Validates the untrusted Webview message before any host-side handler runs. */ +export function validateRpcMessage(value: unknown): RpcMessageValidation { + if (!isPlainObject(value)) { + return invalidMessage("", "", "Invalid bridge request: expected a plain object."); + } + + const id = value["id"]; + if (!Object.hasOwn(value, "id") || typeof id !== "string" || id.trim().length === 0) { + return invalidMessage("", safeMethod(value["method"]), "Invalid bridge request: id must be a non-empty string."); + } + + const method = value["method"]; + if (!Object.hasOwn(value, "method") || typeof method !== "string" || method.trim().length === 0) { + return invalidMessage(id, "", "Invalid bridge request: method must be a non-empty string."); + } + if (!rpcMethods.has(method)) { + return invalidMessage(id, method, `Unknown bridge method: ${method}`); + } + const params = Object.hasOwn(value, "params") ? value["params"] : undefined; + if (!validateParams(method as RpcMethod, params)) { + return invalidMessage(id, method, `Invalid bridge params for method: ${method}`); + } + + return { ok: true, message: { id, method: method as RpcMethod, params } }; +} + +function validateParams(method: RpcMethod, params: unknown): boolean { + switch (method) { + case Methods.CheckWorkspace: + case Methods.GetInputHistory: + case Methods.GetSlashCommands: + case Methods.CheckLoginStatus: + case Methods.Login: + case Methods.Logout: + case Methods.GetExtensionConfig: + case Methods.OpenSettings: + case Methods.OpenFolder: + case Methods.GetModels: + case Methods.GetMCPServers: + case Methods.AbortChat: + case Methods.ResetSession: + case Methods.GetKimiSessions: + case Methods.GetAllKimiSessions: + case Methods.GetRegisteredWorkDirs: + case Methods.BrowseWorkDir: + case Methods.ClearTrackedFiles: + case Methods.ShowLogs: + case Methods.ReloadWebview: + return params === undefined; + + case Methods.AddInputHistory: + return hasString(params, "text"); + case Methods.SaveConfig: + return isPlainObject(params) + && typeof params["model"] === "string" + && isOptionalType(params["thinking"], "boolean") + && isOptionalType(params["effort"], "string") + && isOptionalType(params["effortChanged"], "boolean"); + case Methods.AddMCPServer: + return isMcpServerConfig(params); + case Methods.UpdateMCPServer: + return isMcpUpdate(params); + case Methods.RemoveMCPServer: + case Methods.AuthMCP: + case Methods.ResetAuthMCP: + case Methods.TestMCP: + return hasNonEmptyString(params, "name"); + case Methods.StreamChat: + return isStreamChatParams(params); + case Methods.RespondApproval: + return isPlainObject(params) + && isNonEmptyString(params["requestId"]) + && (params["response"] === "approve" + || params["response"] === "approve_for_session" + || params["response"] === "reject"); + case Methods.RespondQuestion: + return isPlainObject(params) + && isNonEmptyString(params["rpcRequestId"]) + && isNonEmptyString(params["questionRequestId"]) + && isStringRecord(params["answers"]); + case Methods.SetPlanMode: + return hasBoolean(params, "enabled"); + case Methods.SteerChat: + return isPlainObject(params) && isContent(params["content"]); + case Methods.GetProjectFiles: + return params === undefined || ( + isPlainObject(params) + && isOptionalType(params["query"], "string") + && isOptionalType(params["directory"], "string") + ); + case Methods.SetWorkDir: + return isPlainObject(params) && (params["workDir"] === null || typeof params["workDir"] === "string"); + case Methods.LoadKimiSessionHistory: + return hasNonEmptyString(params, "kimiSessionId"); + case Methods.DeleteKimiSession: + return hasNonEmptyString(params, "sessionId"); + case Methods.ForkKimiSession: + return isPlainObject(params) + && isNonEmptyString(params["sessionId"]) + && Number.isInteger(params["turnIndex"]) + && (params["turnIndex"] as number) >= 0; + case Methods.PickMedia: + return isPlainObject(params) + && (params["maxCount"] === undefined + || (Number.isInteger(params["maxCount"]) && (params["maxCount"] as number) >= 0)) + && isOptionalType(params["includeVideo"], "boolean"); + case Methods.OpenFile: + case Methods.OpenFileDiff: + case Methods.CheckFileExists: + case Methods.GetImageDataUri: + return hasString(params, "filePath"); + case Methods.CheckFilesExist: + case Methods.TrackFiles: + return hasStringArray(params, "paths"); + case Methods.RevertFiles: + case Methods.KeepChanges: + return isPlainObject(params) && isOptionalType(params["filePath"], "string"); + } +} + +function isStreamChatParams(value: unknown): boolean { + return isPlainObject(value) + && isContent(value["content"]) + && typeof value["model"] === "string" + && isOptionalType(value["effort"], "string") + && isOptionalType(value["thinking"], "boolean") + && isOptionalType(value["planMode"], "boolean") + && isOptionalType(value["sessionId"], "string"); +} + +function isContent(value: unknown): boolean { + return typeof value === "string" || (Array.isArray(value) && value.every(isContentPart)); +} + +function isContentPart(value: unknown): boolean { + if (!isPlainObject(value) || typeof value["type"] !== "string") return false; + switch (value["type"]) { + case "text": + return typeof value["text"] === "string"; + case "think": + return typeof value["think"] === "string" + && (value["encrypted"] === undefined + || value["encrypted"] === null + || typeof value["encrypted"] === "string"); + case "image_url": + case "audio_url": + case "video_url": { + const media = value[value["type"]]; + return isPlainObject(media) + && typeof media["url"] === "string" + && (media["id"] === undefined || media["id"] === null || typeof media["id"] === "string"); + } + default: + return false; + } +} + +function isMcpUpdate(value: unknown): boolean { + if (!isPlainObject(value)) return false; + if (Object.hasOwn(value, "server")) { + return isNonEmptyString(value["originalName"]) && isMcpServerConfig(value["server"]); + } + return isMcpServerConfig(value); +} + +function isMcpServerConfig(value: unknown): boolean { + return isPlainObject(value) + && isNonEmptyString(value["name"]) + && (value["transport"] === "stdio" || value["transport"] === "http") + && isOptionalType(value["url"], "string") + && isOptionalType(value["command"], "string") + && (value["args"] === undefined || isStringArray(value["args"])) + && (value["env"] === undefined || isStringRecord(value["env"])) + && (value["headers"] === undefined || isStringRecord(value["headers"])) + && (value["auth"] === undefined || value["auth"] === "oauth") + && isOptionalType(value["bearerTokenEnvVar"], "string"); +} + +function hasString(value: unknown, key: string): boolean { + return isPlainObject(value) && typeof value[key] === "string"; +} + +function hasNonEmptyString(value: unknown, key: string): boolean { + return isPlainObject(value) && isNonEmptyString(value[key]); +} + +function hasBoolean(value: unknown, key: string): boolean { + return isPlainObject(value) && typeof value[key] === "boolean"; +} + +function hasStringArray(value: unknown, key: string): boolean { + return isPlainObject(value) && isStringArray(value[key]); +} + +function isStringArray(value: unknown): boolean { + return Array.isArray(value) && value.every((item) => typeof item === "string"); +} + +function isStringRecord(value: unknown): boolean { + return isPlainObject(value) && Object.values(value).every((item) => typeof item === "string"); +} + +function isOptionalType(value: unknown, type: "string" | "boolean"): boolean { + return value === undefined || typeof value === type; +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +function isPlainObject(value: unknown): value is Record { + if (value === null || typeof value !== "object") return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function invalidMessage(id: string, method: string, error: string): RpcMessageValidation { + return { ok: false, id, method, error }; +} + +function safeMethod(value: unknown): string { + return typeof value === "string" && value.length > 0 ? value : ""; +} diff --git a/apps/vscode/shared/errors.ts b/apps/vscode/shared/errors.ts new file mode 100644 index 0000000000000000000000000000000000000000..4640b54ff42fb4a8f96342fb59d452a2af342d20 --- /dev/null +++ b/apps/vscode/shared/errors.ts @@ -0,0 +1,106 @@ +import type { ErrorPhase } from "./types"; + +// Keep the released Webview's error contract local. The upper-case values are +// accepted for sessions restored from the legacy extension; the dotted values +// are emitted by the in-process v1 core. +const LEGACY = { + CLI_NOT_FOUND: "CLI_NOT_FOUND", + SPAWN_FAILED: "SPAWN_FAILED", + ALREADY_STARTED: "ALREADY_STARTED", + STDIN_NOT_WRITABLE: "STDIN_NOT_WRITABLE", + HANDSHAKE_TIMEOUT: "HANDSHAKE_TIMEOUT", + PROCESS_CRASHED: "PROCESS_CRASHED", + LLM_NOT_SET: "LLM_NOT_SET", + LLM_NOT_SUPPORTED: "LLM_NOT_SUPPORTED", + INVALID_STATE: "INVALID_STATE", + CHAT_PROVIDER_ERROR: "CHAT_PROVIDER_ERROR", + SESSION_BUSY: "SESSION_BUSY", + SESSION_CLOSED: "SESSION_CLOSED", + TURN_INTERRUPTED: "TURN_INTERRUPTED", + INVALID_JSON: "INVALID_JSON", + INVALID_REQUEST: "INVALID_REQUEST", + INVALID_PARAMS: "INVALID_PARAMS", + INTERNAL_ERROR: "INTERNAL_ERROR", +} as const; + +// Pre-flight: task didn't start at all or was blocked by "gatekeeper" +export const PREFLIGHT_CODES = new Set([ + LEGACY.CLI_NOT_FOUND, + LEGACY.SPAWN_FAILED, + LEGACY.ALREADY_STARTED, + LEGACY.STDIN_NOT_WRITABLE, + LEGACY.PROCESS_CRASHED, + LEGACY.LLM_NOT_SET, + LEGACY.LLM_NOT_SUPPORTED, + LEGACY.INVALID_STATE, + LEGACY.SESSION_BUSY, + "config.invalid", + "model.not_configured", + "auth.login_required", + "session.not_found", + "session.state_not_found", + "session.state_invalid", + "session.init_failed", + "shell.git_bash_not_found", +]); + +// User-friendly error messages +export const ERROR_MESSAGES: Record = { + // Pre-flight + [LEGACY.CLI_NOT_FOUND]: "Kimi Code CLI not found.", + [LEGACY.SPAWN_FAILED]: "Failed to start Kimi Code CLI.", + [LEGACY.ALREADY_STARTED]: "A session is already running.", + [LEGACY.STDIN_NOT_WRITABLE]: "Failed to communicate with Kimi Code CLI.", + [LEGACY.HANDSHAKE_TIMEOUT]: "Connection timed out.", + [LEGACY.PROCESS_CRASHED]: "Process connection lost.", + + // CLI errors + [LEGACY.LLM_NOT_SET]: "Authentication failed. Please sign in.", + [LEGACY.LLM_NOT_SUPPORTED]: "This model is not supported.", + [LEGACY.INVALID_STATE]: "Please wait for the current operation.", + [LEGACY.CHAT_PROVIDER_ERROR]: "Service temporarily unavailable.", + + // Session errors + [LEGACY.SESSION_BUSY]: "A message is being sent. Please wait.", + [LEGACY.SESSION_CLOSED]: "Session was closed.", + [LEGACY.TURN_INTERRUPTED]: "Stopped by user.", + + // Protocol errors + [LEGACY.INVALID_JSON]: "Communication format error.", + [LEGACY.INVALID_REQUEST]: "Invalid request.", + [LEGACY.INVALID_PARAMS]: "Invalid parameters.", + [LEGACY.INTERNAL_ERROR]: "Internal error occurred.", + + "config.invalid": "Kimi Code configuration is invalid.", + "model.not_configured": "No model is configured. Please sign in or configure a provider.", + "auth.login_required": "Authentication failed. Please sign in.", + "session.not_found": "Session was not found.", + "session.state_not_found": "Session data is missing.", + "session.state_invalid": "Session data is invalid.", + "session.init_failed": "Failed to initialize the session.", + "session.closed": "Session was closed.", + "session.fork_active_turn": "Wait for the current response before forking.", + "turn.agent_busy": "A message is being sent. Please wait.", + "provider.api_error": "Service temporarily unavailable.", + "provider.rate_limit": "Too many requests. Please try again later.", + "provider.auth_error": "Authentication failed. Please sign in again.", + "provider.connection_error": "Could not connect to the model provider.", + "request.prompt_input_empty": "Prompt cannot be empty.", + internal: "Internal error occurred.", +}; + +export function classifyError(code: string): ErrorPhase { + return PREFLIGHT_CODES.has(code) ? "preflight" : "runtime"; +} + +export function getUserMessage(code: string, fallback?: string): string { + return ERROR_MESSAGES[code] || fallback || "An unknown error occurred."; +} + +export function isPreflightError(code: string): boolean { + return PREFLIGHT_CODES.has(code); +} + +export function isUserInterrupt(code: string): boolean { + return code === LEGACY.TURN_INTERRUPTED || code === "turn.cancelled"; +} diff --git a/apps/vscode/shared/fork-turn-index.ts b/apps/vscode/shared/fork-turn-index.ts new file mode 100644 index 0000000000000000000000000000000000000000..43268b57887416fc0fede87f05bb2982d12404c6 --- /dev/null +++ b/apps/vscode/shared/fork-turn-index.ts @@ -0,0 +1,43 @@ +interface ForkTurnItem { + readonly type: string; +} + +interface ForkTurnMessage { + readonly role: "user" | "assistant"; + readonly forkable?: boolean; + readonly steps?: readonly { + readonly items: readonly ForkTurnItem[]; + }[]; +} + +/** Return the core's zero-based user-visible turn index for an assistant bubble. */ +export function getForkTurnIndex( + messages: readonly ForkTurnMessage[], + messageIndex: number, +): number | undefined { + const target = messages[messageIndex]; + if (target?.role !== "assistant" || target.forkable === false) return undefined; + + let visibleTurns = 0; + for (let index = 0; index <= messageIndex; index += 1) { + const message = messages[index]; + if (message === undefined) continue; + if (message.role === "user" && message.forkable !== false) { + visibleTurns += 1; + continue; + } + if (message.role === "assistant") { + visibleTurns += countSteers(message); + } + } + return visibleTurns - 1; +} + +function countSteers(message: ForkTurnMessage): number { + return ( + message.steps?.reduce( + (count, step) => count + step.items.filter((item) => item.type === "steer").length, + 0, + ) ?? 0 + ); +} diff --git a/apps/vscode/shared/legacy-sdk.ts b/apps/vscode/shared/legacy-sdk.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a88ee6a42bf3147c0c4a8c899eea2e9bb0d2d0b --- /dev/null +++ b/apps/vscode/shared/legacy-sdk.ts @@ -0,0 +1,233 @@ +/** + * UI-facing compatibility types for the released Webview. + * + * The extension host adapts v1 Node SDK events into this shape while the UI is + * migrated without a visual rewrite. This file contains no legacy SDK runtime. + */ + +export type ApprovalResponse = 'approve' | 'approve_for_session' | 'reject'; + +export type ContentPart = + | { type: 'text'; text: string } + | { type: 'think'; think: string; encrypted?: string | null } + | { type: 'image_url'; image_url: { url: string; id?: string | null } } + | { type: 'audio_url'; audio_url: { url: string; id?: string | null } } + | { type: 'video_url'; video_url: { url: string; id?: string | null } }; + +export interface BriefBlock { + type: 'brief'; + text: string; +} + +export interface DiffBlock { + type: 'diff'; + path: string; + old_text: string; + new_text: string; +} + +export interface TodoBlock { + type: 'todo'; + items: Array<{ title: string; status: 'pending' | 'in_progress' | 'done' }>; +} + +export interface ShellBlock { + type: 'shell'; + language: string; + command: string; +} + +export interface UnknownBlock { + type: string; + data?: Record; + [key: string]: unknown; +} + +export type DisplayBlock = BriefBlock | DiffBlock | TodoBlock | ShellBlock | UnknownBlock; + +export interface ToolCall { + type: 'function'; + id: string; + function: { name: string; arguments?: string | null }; + extras?: Record | null; +} + +export interface ToolReturnValue { + is_error: boolean; + output: string | ContentPart[]; + message: string; + display: DisplayBlock[]; + extras?: Record | null; +} + +export interface ToolResult { + tool_call_id: string; + return_value: ToolReturnValue; +} + +export interface TurnBegin { + user_input: string | ContentPart[]; +} + +export interface TokenUsage { + input_other: number; + output: number; + input_cache_read: number; + input_cache_creation: number; +} + +export interface StatusUpdate { + context_usage?: number | null; + token_usage?: TokenUsage | null; + message_id?: string | null; + plan_mode?: boolean | null; + model?: string | null; + thinking_effort?: string | null; + retrying?: { + next_attempt: number; + max_attempts: number; + delay_ms: number; + message: string; + } | null; +} + +export interface ApprovalRequestPayload { + id: string; + tool_call_id: string; + sender: string; + action: string; + description: string; + display?: DisplayBlock[]; +} + +export interface QuestionOption { + label: string; + description?: string; +} + +export interface QuestionItem { + question: string; + header?: string; + options: QuestionOption[]; + multi_select?: boolean; +} + +export interface QuestionRequest { + id: string; + tool_call_id: string; + questions: QuestionItem[]; +} + +export interface QuestionResponse { + request_id: string; + answers: Record; +} + +export interface SubagentEvent { + parent_tool_call_id: string; + event: LegacyWireEvent; +} + +export type LegacyWireEvent = + | { type: 'TurnBegin'; payload: TurnBegin & { forkable?: boolean } } + | { type: 'TurnEnd'; payload: Record } + | { type: 'StepBegin'; payload: { n: number } } + | { type: 'StepInterrupted'; payload: Record } + | { type: 'CompactionBegin'; payload: Record } + | { type: 'CompactionEnd'; payload: Record } + | { type: 'StatusUpdate'; payload: StatusUpdate } + | { type: 'ContentPart'; payload: ContentPart } + | { type: 'ToolCall'; payload: ToolCall } + | { type: 'ToolCallPart'; payload: { tool_call_id?: string; arguments_part?: string | null } } + | { type: 'ToolResult'; payload: ToolResult } + | { type: 'SteerInput'; payload: { user_input: string | ContentPart[] } } + | { type: 'SubagentEvent'; payload: SubagentEvent } + | { type: string; payload: unknown }; + +export type StreamEvent = + | LegacyWireEvent + | { type: 'ApprovalRequest'; payload: ApprovalRequestPayload } + | { type: 'QuestionRequest'; payload: QuestionRequest } + | { type: 'error'; code: string; message: string; raw?: string }; + +export interface RunResult { + status: 'finished' | 'cancelled' | 'max_steps_reached'; + steps?: number; +} + +export interface SlashCommandInfo { + name: string; + description: string; + aliases: string[]; +} + +export interface ModelConfig { + id: string; + name: string; + provider: string; + capabilities: string[]; + adaptive_thinking?: boolean; + support_efforts?: string[]; + default_effort?: string; +} + +export interface KimiConfig { + defaultModel: string | null; + defaultThinking: boolean; + defaultThinkingEffort?: string; + models: ModelConfig[]; +} + +/** Placeholder returned to the Webview instead of a stored MCP credential. */ +export const MCP_SECRET_MASK = '••••••••'; + +export interface MCPServerConfig { + name: string; + transport: 'http' | 'stdio'; + url?: string; + command?: string; + args?: string[]; + env?: Record; + headers?: Record; + auth?: 'oauth'; + bearerTokenEnvVar?: string; + /** Unified management view tags; absent on cores predating the management plane. */ + source?: 'global' | 'plugin' | 'caller'; + /** global: defining file path; plugin: plugin id. */ + origin?: string; + /** false for plugin / project-layer entries — the panel hides mutating actions for them. */ + mutable?: boolean; +} + +export interface UpdateMCPServerRequest { + originalName: string; + server: MCPServerConfig; +} + +export interface SessionInfo { + id: string; + workDir: string; + updatedAt: number; + brief: string; +} + +export type ThinkingMode = 'none' | 'switch' | 'always' | 'effort'; + +export interface MCPTestResult { + success: boolean; + output: string; +} + +export interface LoginResult { + success: boolean; + error?: string; +} + +export function formatContentOutput(output: string | ContentPart[]): string { + if (typeof output === 'string') return output; + if (!Array.isArray(output)) return JSON.stringify(output); + return output + .map((item) => item.type === 'text' ? item.text : `[${item.type}]`) + .filter(Boolean) + .join('\n'); +} diff --git a/apps/vscode/shared/types.ts b/apps/vscode/shared/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..7945484901931f9fc18425f197c2c4e7929786c8 --- /dev/null +++ b/apps/vscode/shared/types.ts @@ -0,0 +1,73 @@ +import type { RunResult, StreamEvent } from "./legacy-sdk"; + +export interface SessionConfig { + model: string; + thinking?: boolean; + effort?: string; + /** + * Whether the user explicitly changed the effort. Re-confirming the effort + * already shown is not an explicit choice: the model is persisted but the + * stored effort preference is left alone (mirrors the TUI's + * persistModelSelection). Treated as true when omitted. + */ + effortChanged?: boolean; +} + +export interface ProjectFile { + path: string; + name: string; + isDirectory: boolean; + /** Matched-character offsets into `path`, for mention-style highlighting. */ + matchPositions?: number[]; +} + +export interface FileChange { + path: string; + status: "Modified" | "Added" | "Deleted"; + additions: number; + deletions: number; +} + +export interface ExtensionConfig { + yoloMode: boolean; + autosave: boolean; + useCtrlEnterToSend: boolean; + enableNewConversationShortcut: boolean; + showThinkingContent: boolean; + showThinkingExpanded: boolean; + version: string; +} + +export interface WorkspaceStatus { + hasWorkspace: boolean; + path?: string; + workspaceRoot?: string; +} + +export type ErrorPhase = "preflight" | "runtime"; + +export interface StreamError { + type: "error"; + code: string; + message: string; + detail?: string; // 原始服务器错误信息 + phase: ErrorPhase; + /** + * `false` marks a mid-turn warning: the turn is still running, so UIs must + * not treat it as turn-ending. Do not unlock the composer, offer Retry, or + * flush the queued messages for non-terminal errors. + */ + terminal?: boolean; +} + +export type UIStreamEvent = + | { type: "session_start"; sessionId: string; model?: string; _sessionId?: string } + | { type: "stream_complete"; result: RunResult; _sessionId?: string } + | (StreamError & { _sessionId?: string }) + | (StreamEvent & { _sessionId?: string }); + +export interface LoginStatus { + loggedIn: boolean; +} + +export type { QuestionRequest, QuestionItem, QuestionOption, QuestionResponse } from "./legacy-sdk"; diff --git a/apps/vscode/shared/utils.ts b/apps/vscode/shared/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..625f6c9ea3f85d2dde210ef197b7e497b7e9f568 --- /dev/null +++ b/apps/vscode/shared/utils.ts @@ -0,0 +1,3 @@ +export function cleanSystemTags(text: string): string { + return text.replace(/.*?<\/system>\s*/gs, "").trim(); +} diff --git a/apps/vscode/test/app-init.test.ts b/apps/vscode/test/app-init.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..85393df1460e6d8c248e352b1d6db018ab56b4ee --- /dev/null +++ b/apps/vscode/test/app-init.test.ts @@ -0,0 +1,86 @@ +/** + * Scenario: App-level view routing after init, across login state transitions. + * Responsibilities: the sign-in screen must stay reachable from every state — in + * particular the no-models state (a managed OAuth token exists but config.toml + * has no models, e.g. a first login whose model provisioning failed after the + * device flow already persisted the token), where Reload alone can never change + * the on-disk state and the user would otherwise be stranded. + * Wiring: resolveAppView is pure; the bridge and toast boundaries are mocked away. + * Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts test/app-init.test.ts + */ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/services", () => ({ + bridge: {}, + Events: {}, +})); +vi.mock("@/components/ui/sonner", () => ({ + toast: { error: vi.fn(), warning: vi.fn() }, +})); + +import { resolveAppView, type AppStatus } from "../webview-ui/src/hooks/useAppInit"; + +function resolve( + status: AppStatus, + options: { modelsCount?: number; skippedLogin?: boolean; showLogin?: boolean } = {}, +) { + return resolveAppView({ + status, + modelsCount: options.modelsCount ?? 0, + skippedLogin: options.skippedLogin ?? false, + showLogin: options.showLogin ?? false, + }); +} + +describe("resolveAppView", () => { + it("routes a brand-new user (no token, no models) to the login screen", () => { + expect(resolve("not-logged-in")).toEqual({ view: "login" }); + }); + + it("routes a skipped login without models to no-models with a sign-in path", () => { + expect(resolve("not-logged-in", { skippedLogin: true })).toEqual({ + view: "status", + status: "no-models", + canGoToLogin: true, + }); + }); + + it("routes a skipped login with models to the main view", () => { + expect(resolve("not-logged-in", { skippedLogin: true, modelsCount: 2 })).toEqual({ + view: "main", + }); + }); + + it("keeps a sign-in path in the no-models trap (token without model config)", () => { + // Regression: this state previously rendered "Model setup required" with only + // a Reload button, making the login screen unreachable for affected users. + expect(resolve("no-models")).toEqual({ + view: "status", + status: "no-models", + canGoToLogin: true, + }); + }); + + it("routes to the login screen when the user asks for it from any state", () => { + expect(resolve("no-models", { showLogin: true })).toEqual({ view: "login" }); + expect(resolve("ready", { showLogin: true, modelsCount: 1 })).toEqual({ view: "login" }); + }); + + it("routes a no-models user who skips again back to no-models with a sign-in path", () => { + expect(resolve("no-models", { skippedLogin: true })).toEqual({ + view: "status", + status: "no-models", + canGoToLogin: true, + }); + }); + + it("routes ready to the main view", () => { + expect(resolve("ready", { modelsCount: 1 })).toEqual({ view: "main" }); + }); + + it("routes non-login error statuses to status screens without a sign-in path", () => { + for (const status of ["loading", "no-workspace", "runtime-error"] as const) { + expect(resolve(status)).toEqual({ view: "status", status, canGoToLogin: false }); + } + }); +}); diff --git a/apps/vscode/test/baseline.manager.test.ts b/apps/vscode/test/baseline.manager.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2554f288c5c037d313ba599221cfbd8bb8480462 --- /dev/null +++ b/apps/vscode/test/baseline.manager.test.ts @@ -0,0 +1,412 @@ +/** + * Scenario: VSCode-owned file baselines for new, migrated, and forked sessions. + * Responsibilities: capture originals, show changes, keep/undo, persist legacy tombstones, and reject unsafe paths. + * Wiring: real temporary workspace/global-storage/legacy files; no stubbed collaborators. + * Run: pnpm --filter kimi-code test -- baseline.manager.test.ts + */ +import { existsSync, writeFileSync } from 'node:fs'; +import { chmod, mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { + BaselineManager, + type BaselineSession, +} from '../src/managers/baseline.manager'; + +let root: string; +let workDir: string; +let storageRoot: string; +let manager: BaselineManager; + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'vscode-baseline-')); + workDir = join(root, 'workspace'); + storageRoot = join(root, 'global-storage'); + await mkdir(workDir, { recursive: true }); + manager = new BaselineManager(storageRoot); +}); + +afterEach(async () => { + await rm(root, { recursive: true, force: true }); +}); + +describe('file baselines (capture, compare, keep, and undo)', () => { + it('keeps the first original when overlapping captures race', async () => { + const session = createSession(); + const filePath = join(workDir, 'src', 'app.ts'); + await mkdir(join(workDir, 'src'), { recursive: true }); + writeFileSync(filePath, 'original\n', 'utf-8'); + + const firstCapture = manager.capture(session, filePath); + writeFileSync(filePath, 'after first edit\n', 'utf-8'); + const secondCapture = manager.capture(session, filePath); + await Promise.all([firstCapture, secondCapture]); + + expect(await manager.getContent(session, filePath)).toBe('original\n'); + }); + + it('returns the captured original after the manager is recreated', async () => { + const session = createSession(); + const filePath = join(workDir, 'app.ts'); + await writeFile(filePath, 'persisted original\n', 'utf-8'); + await manager.capture(session, filePath); + + const reloadedManager = new BaselineManager(storageRoot); + + await expect(reloadedManager.getContent(session, filePath)).resolves.toBe( + 'persisted original\n', + ); + }); + + it('isolates the same session id between different Kimi homes', async () => { + const session = createSession(); + const filePath = join(workDir, 'app.ts'); + const firstHome = new BaselineManager(storageRoot, join(root, 'home-a')); + const secondHome = new BaselineManager(storageRoot, join(root, 'home-b')); + await writeFile(filePath, 'home A original\n', 'utf-8'); + await firstHome.capture(session, filePath); + await writeFile(filePath, 'home B original\n', 'utf-8'); + await secondHome.capture(session, filePath); + + await expect(firstHome.getContent(session, filePath)).resolves.toBe('home A original\n'); + await expect(secondHome.getContent(session, filePath)).resolves.toBe('home B original\n'); + }); + + it('reports a modified file when current content differs from its original', async () => { + const session = createSession(); + const filePath = join(workDir, 'app.ts'); + await writeFile(filePath, 'before\n', 'utf-8'); + await manager.capture(session, filePath); + await writeFile(filePath, 'after\n', 'utf-8'); + + await expect(manager.getChanges(session)).resolves.toEqual([ + { path: 'app.ts', status: 'Modified', additions: 1, deletions: 1 }, + ]); + }); + + it('reports an added file when it did not exist at capture time', async () => { + const session = createSession(); + const filePath = join(workDir, 'new.ts'); + await manager.capture(session, filePath); + await writeFile(filePath, 'new content\n', 'utf-8'); + + await expect(manager.getChanges(session)).resolves.toEqual([ + { path: 'new.ts', status: 'Added', additions: 2, deletions: 0 }, + ]); + }); + + it('reports a deleted file when an existing original is removed', async () => { + const session = createSession(); + const filePath = join(workDir, 'old.ts'); + await writeFile(filePath, 'line one\nline two', 'utf-8'); + await manager.capture(session, filePath); + await unlink(filePath); + + await expect(manager.getChanges(session)).resolves.toEqual([ + { path: 'old.ts', status: 'Deleted', additions: 0, deletions: 2 }, + ]); + }); + + it('removes a newly created file when undo restores its missing original', async () => { + const session = createSession(); + const filePath = join(workDir, 'new.ts'); + await manager.capture(session, filePath); + await writeFile(filePath, 'created\n', 'utf-8'); + + await manager.undo(session, filePath); + + expect(existsSync(filePath)).toBe(false); + }); + + it('restores an existing empty file without deleting it when undo runs', async () => { + const session = createSession(); + const filePath = join(workDir, 'empty.ts'); + await writeFile(filePath, '', 'utf-8'); + await manager.capture(session, filePath); + await writeFile(filePath, 'changed', 'utf-8'); + + await manager.undo(session, filePath); + + expect(existsSync(filePath)).toBe(true); + await expect(readFile(filePath, 'utf-8')).resolves.toBe(''); + }); + + it('preserves CRLF bytes when undo restores the original file', async () => { + const session = createSession(); + const filePath = join(workDir, 'windows.txt'); + await writeFile(filePath, 'first\r\nsecond\r\n', 'utf-8'); + await manager.capture(session, filePath); + await writeFile(filePath, 'changed\n', 'utf-8'); + + await manager.undo(session, filePath); + + await expect(readFile(filePath, 'utf-8')).resolves.toBe('first\r\nsecond\r\n'); + }); + + it('captures a new original after keep accepts the previous changes', async () => { + const session = createSession(); + const filePath = join(workDir, 'app.ts'); + await writeFile(filePath, 'first\n', 'utf-8'); + await manager.capture(session, filePath); + await writeFile(filePath, 'accepted\n', 'utf-8'); + await manager.keep(session, filePath); + + await manager.capture(session, filePath); + await writeFile(filePath, 'next edit\n', 'utf-8'); + + expect(await manager.getContent(session, filePath)).toBe('accepted\n'); + }); + + it('restores every tracked original when undo all runs', async () => { + const session = createSession(); + await writeFile(join(workDir, 'one.ts'), 'one before', 'utf-8'); + await writeFile(join(workDir, 'two.ts'), 'two before', 'utf-8'); + await manager.capture(session, 'one.ts'); + await manager.capture(session, 'two.ts'); + await writeFile(join(workDir, 'one.ts'), 'one after', 'utf-8'); + await writeFile(join(workDir, 'two.ts'), 'two after', 'utf-8'); + + await manager.undoAll(session); + + await expect(readFile(join(workDir, 'one.ts'), 'utf-8')).resolves.toBe('one before'); + await expect(readFile(join(workDir, 'two.ts'), 'utf-8')).resolves.toBe('two before'); + }); + + it('refuses undo all when a tracked directory now links outside the workspace', async () => { + const session = createSession(); + const trackedDir = join(workDir, 'src'); + const outsideDir = join(root, 'outside'); + await mkdir(trackedDir); + await mkdir(outsideDir); + await writeFile(join(trackedDir, 'app.ts'), 'original'); + await manager.capture(session, 'src/app.ts'); + await rm(trackedDir, { recursive: true }); + await writeFile(join(outsideDir, 'app.ts'), 'outside'); + await symlink(outsideDir, trackedDir, process.platform === 'win32' ? 'junction' : 'dir'); + + await expect(manager.undoAll(session)).rejects.toThrow('outside the session workspace'); + await expect(readFile(join(outsideDir, 'app.ts'), 'utf-8')).resolves.toBe('outside'); + }); + + it('removes every visible change when keep all runs', async () => { + const session = createSession(); + await writeFile(join(workDir, 'one.ts'), 'one before', 'utf-8'); + await writeFile(join(workDir, 'two.ts'), 'two before', 'utf-8'); + await manager.capture(session, 'one.ts'); + await manager.capture(session, 'two.ts'); + await writeFile(join(workDir, 'one.ts'), 'one after', 'utf-8'); + await writeFile(join(workDir, 'two.ts'), 'two after', 'utf-8'); + + await manager.keepAll(session); + + await expect(manager.getChanges(session)).resolves.toEqual([]); + }); +}); + +describe('legacy baselines (fallback, tombstones, and fork isolation)', () => { + it('returns the legacy original when no local baseline exists', async () => { + const { session } = await createLegacySession('legacy original\n'); + await writeFile(join(workDir, 'app.ts'), 'current content\n', 'utf-8'); + + await expect(manager.getContent(session, 'app.ts')).resolves.toBe('legacy original\n'); + }); + + it('reports a legacy file when current content differs from its original', async () => { + const { session } = await createLegacySession('legacy original\n'); + await writeFile(join(workDir, 'app.ts'), 'current content\n', 'utf-8'); + + await expect(manager.getChanges(session)).resolves.toEqual([ + { path: 'app.ts', status: 'Modified', additions: 1, deletions: 1 }, + ]); + }); + + it('restores a legacy original without mutating its snapshot when undo runs', async () => { + const { session, baselinePath } = await createLegacySession('legacy original\n'); + await writeFile(join(workDir, 'app.ts'), 'current content\n', 'utf-8'); + + await manager.undo(session, 'app.ts'); + + await expect(readFile(join(workDir, 'app.ts'), 'utf-8')).resolves.toBe('legacy original\n'); + await expect(readFile(baselinePath, 'utf-8')).resolves.toBe('legacy original\n'); + }); + + it('persists a keep tombstone for legacy content without deleting the old snapshot', async () => { + const { session, baselinePath } = await createLegacySession('legacy original\n'); + await writeFile(join(workDir, 'app.ts'), 'accepted content\n', 'utf-8'); + await manager.keep(session, 'app.ts'); + + const reloadedManager = new BaselineManager(storageRoot); + + await expect(reloadedManager.getChanges(session)).resolves.toEqual([]); + await expect(readFile(baselinePath, 'utf-8')).resolves.toBe('legacy original\n'); + }); + + it('uses a materialized local original before the fork target legacy fallback', async () => { + const source = createSession('ses-source'); + const filePath = join(workDir, 'app.ts'); + await writeFile(filePath, 'local original\n', 'utf-8'); + await manager.capture(source, filePath); + const { session: target } = await createLegacySession('legacy original\n', 'ses-target'); + + await manager.materializeToFork(source, target); + + await expect(manager.getContent(target, 'app.ts')).resolves.toBe('local original\n'); + }); + + it('keeps a fork baseline readable when the legacy source is later deleted', async () => { + const { session: source, legacySessionDir } = await createLegacySession( + 'legacy original\n', + 'ses-source', + ); + const target = createSession('ses-target', source.metadata); + await manager.materializeToFork(source, target); + await rm(legacySessionDir, { recursive: true, force: true }); + + await expect(manager.getContent(target, 'app.ts')).resolves.toBe('legacy original\n'); + }); + + it('keeps the source baseline visible when the fork accepts its own copy', async () => { + const { session: source } = await createLegacySession('legacy original\n', 'ses-source'); + const target = createSession('ses-target', source.metadata); + await manager.materializeToFork(source, target); + + await manager.keep(target, 'app.ts'); + + await expect(manager.getContent(source, 'app.ts')).resolves.toBe('legacy original\n'); + }); + + it('preserves accepted legacy paths when a fork is materialized', async () => { + const { session: source } = await createLegacySession('legacy original\n', 'ses-source'); + const target = createSession('ses-target', source.metadata); + await manager.keep(source, 'app.ts'); + + await manager.materializeToFork(source, target); + + await expect(manager.getContent(target, 'app.ts')).rejects.toThrow('No baseline exists'); + }); +}); + +describe('baseline boundaries (errors, cleanup, and platform paths)', () => { + it.skipIf(process.platform === 'win32')( + 'rejects an unreadable original without recording an empty baseline', + async () => { + const session = createSession(); + const filePath = join(workDir, 'unreadable.ts'); + await writeFile(filePath, 'original', 'utf-8'); + await chmod(filePath, 0o000); + + try { + await expect(manager.capture(session, filePath)).rejects.toThrow( + 'Unable to capture original file', + ); + await expect(manager.getContent(session, filePath)).rejects.toThrow( + 'No baseline exists', + ); + } finally { + await chmod(filePath, 0o600); + } + }, + ); + + it('rejects a directory path without recording an empty baseline', async () => { + const session = createSession(); + const directoryPath = join(workDir, 'not-a-file'); + await mkdir(directoryPath); + + await expect(manager.capture(session, directoryPath)).rejects.toThrow( + 'is not a regular file', + ); + await expect(manager.getContent(session, directoryPath)).rejects.toThrow( + 'No baseline exists', + ); + }); + + it('throws a clear error when requested baseline content does not exist', async () => { + await expect(manager.getContent(createSession(), 'missing.ts')).rejects.toThrow( + 'No baseline exists for "missing.ts"', + ); + }); + + it('rejects a relative path that escapes the workspace', async () => { + await expect(manager.capture(createSession(), '../outside.ts')).rejects.toThrow( + 'is outside workspace', + ); + }); + + it('normalizes Windows drive case for an in-workspace baseline', async () => { + const session: BaselineSession = { id: 'ses-windows', workDir: 'C:\\Workspace' }; + await manager.capture(session, 'C:\\Workspace\\src\\new.ts'); + + await expect( + manager.getContent(session, 'c:\\WORKSPACE\\SRC\\NEW.ts'), + ).resolves.toBe(''); + }); + + it('rejects a Windows path on another drive', async () => { + const session: BaselineSession = { id: 'ses-windows', workDir: 'C:\\Workspace' }; + + await expect(manager.capture(session, 'D:\\Workspace\\file.ts')).rejects.toThrow( + 'is outside workspace', + ); + }); + + it('normalizes case differences in an in-workspace UNC path', async () => { + const session: BaselineSession = { + id: 'ses-unc', + workDir: '\\\\Server\\Share\\Workspace', + }; + await manager.capture(session, '\\\\server\\share\\workspace\\src\\new.ts'); + + await expect(manager.getContent(session, 'src\\new.ts')).resolves.toBe(''); + }); + + it('rejects a UNC path from another share', async () => { + const session: BaselineSession = { + id: 'ses-unc', + workDir: '\\\\Server\\Share\\Workspace', + }; + + await expect( + manager.capture(session, '\\\\Server\\OtherShare\\Workspace\\file.ts'), + ).rejects.toThrow('is outside workspace'); + }); + + it('removes persisted originals when the session baseline is deleted', async () => { + const session = createSession(); + const filePath = join(workDir, 'app.ts'); + await writeFile(filePath, 'original', 'utf-8'); + await manager.capture(session, filePath); + + await manager.deleteSession(session.id); + + await expect(manager.getContent(session, filePath)).rejects.toThrow('No baseline exists'); + }); +}); + +function createSession( + id = 'ses-local', + metadata?: Readonly>, +): BaselineSession { + return { id, workDir, metadata }; +} + +async function createLegacySession( + content: string, + id = 'ses-legacy', +): Promise<{ + session: BaselineSession; + legacySessionDir: string; + baselinePath: string; +}> { + const legacySessionDir = join(root, `legacy-${id}`); + const baselinePath = join(legacySessionDir, 'baseline', 'app.ts'); + await mkdir(join(legacySessionDir, 'baseline'), { recursive: true }); + await writeFile(baselinePath, content, 'utf-8'); + return { + session: createSession(id, { kimi_cli_source_path: legacySessionDir }), + legacySessionDir, + baselinePath, + }; +} diff --git a/apps/vscode/test/bridge-handler.test.ts b/apps/vscode/test/bridge-handler.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..69dd7d3d15100daa50da340f3501bf07a4e925b2 --- /dev/null +++ b/apps/vscode/test/bridge-handler.test.ts @@ -0,0 +1,695 @@ +/** + * Scenario: untrusted Webview RPC messages cross into the VS Code extension host. + * Responsibilities: validate requests, preserve public model metadata, omit private paths, and recover visibly from persisted state errors. + * Wiring: the real BridgeHandler and handlers; VS Code and the public Node SDK harness boundary are replaced. + * Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/bridge-handler.test.ts + */ +import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest"; +import type * as vscode from "vscode"; + +import { Methods } from "../shared/bridge"; +import { BridgeHandler } from "../src/bridge-handler"; + +const host = vi.hoisted(() => { + const watcher = { + onDidChange: vi.fn(), + onDidCreate: vi.fn(), + onDidDelete: vi.fn(), + dispose: vi.fn(), + }; + const harness = { + homeDir: "/tmp/kimi-code-test-home", + close: vi.fn(async () => undefined), + getConfig: vi.fn(), + setConfig: vi.fn(async () => undefined), + listSessions: vi.fn(async () => []), + resumeSession: vi.fn(), + forkSession: vi.fn(), + deleteSession: vi.fn(async () => undefined), + }; + const showWarningMessage = vi.fn(async () => undefined as string | undefined); + + class Uri { + readonly scheme = "file"; + readonly authority = ""; + readonly path: string; + + constructor(readonly fsPath: string) { + this.path = fsPath; + } + + static joinPath(base: Uri, ...segments: string[]): Uri { + return new Uri(join(base.fsPath, ...segments)); + } + + toString(): string { + return `file://${this.path}`; + } + } + + return { + Uri, + watcher, + harness, + createKimiHarness: vi.fn(() => harness), + showWarningMessage, + workspaceFolders: [] as Array<{ uri: Uri }>, + }; +}); + +vi.mock("vscode", () => ({ + Uri: host.Uri, + workspace: { + get workspaceFolders() { + return host.workspaceFolders; + }, + getConfiguration: () => ({ get: (_key: string, fallback: unknown) => fallback }), + createFileSystemWatcher: () => host.watcher, + textDocuments: [], + }, + window: { showWarningMessage: host.showWarningMessage }, +})); + +vi.mock("@moonshot-ai/kimi-code-sdk", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + createKimiHarness: () => host.createKimiHarness(), + }; +}); + +let bridge: BridgeHandler; +let root: string; +let showLogs: Mock<() => void>; +let writeLog: Mock<(message: string) => void>; +let workspaceState: { get: ReturnType; update: ReturnType }; + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), "kimi-vscode-bridge-")); + host.workspaceFolders.splice(0, host.workspaceFolders.length, { uri: new host.Uri(root) }); + showLogs = vi.fn(); + writeLog = vi.fn(); + host.harness.resumeSession.mockReset(); + host.harness.getConfig.mockReset(); + host.harness.getConfig.mockResolvedValue({ models: {} }); + host.createKimiHarness.mockImplementation(() => host.harness); + host.showWarningMessage.mockReset(); + host.showWarningMessage.mockResolvedValue(undefined); + workspaceState = { get: vi.fn((_key, fallback) => fallback), update: vi.fn() }; + bridge = new BridgeHandler( + vi.fn(), + workspaceState as unknown as vscode.Memento, + join(root, "global-storage"), + vi.fn(), + showLogs, + writeLog, + ); +}); + +afterEach(async () => { + await bridge.dispose(); + vi.clearAllMocks(); + vi.unstubAllEnvs(); + await rm(root, { recursive: true, force: true }); +}); + +describe("Engine startup", () => { + function constructBridge(): void { + new BridgeHandler( + vi.fn(), + workspaceState as unknown as vscode.Memento, + join(root, "global-storage-2"), + vi.fn(), + showLogs, + writeLog, + ); + } + + it("surfaces the failure when the engine cannot start", () => { + host.createKimiHarness.mockImplementationOnce(() => { + throw new Error("engine boom"); + }); + + expect(constructBridge).toThrow(/^Failed to start the Kimi engine: engine boom\.$/); + }); +}); + +describe("Webview RPC boundary (validates requests before host dispatch)", () => { + it("returns a readable error when the envelope is not a plain object", async () => { + const result = await bridge.handle([], "view-1"); + + expect(result).toEqual({ + id: "", + error: "Invalid bridge request: expected a plain object.", + }); + }); + + it("does not execute a known handler when the request id is blank", async () => { + const result = await bridge.handle({ id: " ", method: Methods.ShowLogs }, "view-1"); + + expect(result).toEqual({ + id: "", + error: "Invalid bridge request: id must be a non-empty string.", + }); + expect(showLogs).not.toHaveBeenCalled(); + }); + + it("reports aborted: false when the view has no runtime to cancel", async () => { + const result = await bridge.handle({ id: "rpc-1", method: Methods.AbortChat }, "view-1"); + + expect(result).toEqual({ id: "rpc-1", result: { aborted: false } }); + }); + + it("cancels the view's runtime when aborting a chat", async () => { + const cancel = vi.fn(async () => undefined); + vi.spyOn(bridge.runtime, "getSessionForView").mockReturnValue({ cancel } as never); + + const result = await bridge.handle({ id: "rpc-1", method: Methods.AbortChat }, "view-1"); + + expect(result).toEqual({ id: "rpc-1", result: { aborted: true } }); + expect(cancel).toHaveBeenCalledOnce(); + }); + + it.each(["missingMethod", "toString", "constructor", "__proto__"])( + "does not dispatch the unknown or prototype method %s", + async (method) => { + const result = await bridge.handle({ id: "rpc-1", method }, "view-1"); + + expect(result).toEqual({ id: "rpc-1", error: `Unknown bridge method: ${method}` }); + expect(showLogs).not.toHaveBeenCalled(); + }, + ); + + it("does not execute a no-params handler when a payload is supplied", async () => { + const result = await bridge.handle( + { id: "rpc-1", method: Methods.ShowLogs, params: {} }, + "view-1", + ); + + expect(result).toEqual({ + id: "rpc-1", + error: "Invalid bridge params for method: showLogs", + }); + expect(showLogs).not.toHaveBeenCalled(); + }); + + it("does not execute an object-payload handler when a required field has the wrong type", async () => { + const result = await bridge.handle( + { id: "rpc-1", method: Methods.AddInputHistory, params: { text: 42 } }, + "view-1", + ); + + expect(result).toEqual({ + id: "rpc-1", + error: "Invalid bridge params for method: addInputHistory", + }); + expect(workspaceState.update).not.toHaveBeenCalled(); + }); + + it("dispatches a valid request through the existing bridge surface", async () => { + const result = await bridge.handle({ id: "rpc-1", method: Methods.ShowLogs }, "view-1"); + + expect(result).toEqual({ id: "rpc-1", result: { ok: true } }); + expect(showLogs).toHaveBeenCalledOnce(); + }); + + it("keeps provider identity when configured models share a display name", async () => { + host.harness.getConfig.mockResolvedValueOnce({ + defaultModel: "openai/shared", + models: { + "openai/shared": { + provider: "openai", + model: "shared", + displayName: "Shared", + maxContextSize: 128_000, + }, + "proxy/shared": { + provider: "company-proxy", + model: "shared", + displayName: "Shared", + maxContextSize: 128_000, + }, + }, + }); + + const result = await bridge.handle({ id: "rpc-models", method: Methods.GetModels }, "view-1"); + + expect(result).toMatchObject({ + id: "rpc-models", + result: { + defaultModel: "openai/shared", + models: [ + { id: "openai/shared", name: "Shared", provider: "openai" }, + { id: "proxy/shared", name: "Shared", provider: "company-proxy" }, + ], + }, + }); + }); + + it("preserves adaptive thinking metadata in the Webview model list", async () => { + host.harness.getConfig.mockResolvedValueOnce({ + defaultModel: "anthropic/claude", + models: { + "anthropic/claude": { + provider: "anthropic", + model: "claude-sonnet", + maxContextSize: 200_000, + adaptiveThinking: true, + }, + }, + }); + + const result = await bridge.handle({ id: "rpc-models", method: Methods.GetModels }, "view-1"); + + expect(result).toMatchObject({ + result: { + models: [{ + id: "anthropic/claude", + name: "claude-sonnet", + provider: "anthropic", + adaptive_thinking: true, + }], + }, + }); + }); + + it("resolves the fallback-profile default effort with the provider type", async () => { + // claude-latest declares efforts but no default; the Anthropic fallback + // profile only matches when the provider type joins the resolution. + host.harness.getConfig.mockResolvedValueOnce({ + defaultModel: "custom/claude", + providers: { + custom: { type: "anthropic", apiKey: "test-key" }, + }, + models: { + "custom/claude": { + provider: "custom", + model: "claude-latest", + supportEfforts: ["low", "medium", "high", "xhigh", "max"], + }, + }, + }); + + const result = await bridge.handle({ id: "rpc-models", method: Methods.GetModels }, "view-1"); + + expect(result).toMatchObject({ + result: { + models: [{ + id: "custom/claude", + support_efforts: ["low", "medium", "high", "xhigh", "max"], + default_effort: "high", + }], + }, + }); + }); + + it("does not expose the session storage path when listing sessions", async () => { + host.harness.listSessions.mockResolvedValueOnce([ + { + id: "session-1", + workDir: root, + sessionDir: "/private/kimi/sessions/session-1", + updatedAt: 123, + title: "Visible title", + }, + ] as never); + + const result = await bridge.handle( + { id: "rpc-1", method: Methods.GetKimiSessions }, + "view-1", + ); + + expect(result).toEqual({ + id: "rpc-1", + result: [{ id: "session-1", workDir: root, updatedAt: 123, brief: "Visible title" }], + }); + expect(JSON.stringify(result)).not.toContain("/private/kimi/sessions"); + }); + + it("does not expose the session storage path when forking a session", async () => { + const source = { + id: "session-1", + workDir: root, + sessionDir: "/private/kimi/sessions/session-1", + updatedAt: 123, + }; + const target = { + id: "session-2", + workDir: root, + sessionDir: "/private/kimi/sessions/session-2", + updatedAt: 124, + }; + host.harness.listSessions.mockResolvedValueOnce([source] as never); + host.harness.forkSession.mockResolvedValueOnce({ summary: target, close: vi.fn() }); + + const result = await bridge.handle( + { + id: "rpc-1", + method: Methods.ForkKimiSession, + params: { sessionId: "session-1", turnIndex: 0 }, + }, + "view-1", + ); + + expect(result).toEqual({ id: "rpc-1", result: { sessionId: "session-2" } }); + expect(JSON.stringify(result)).not.toContain("/private/kimi/sessions"); + }); + + it("runs a fork through the active session cancellation boundary", async () => { + const source = { + id: "session-1", + workDir: root, + sessionDir: "/private/kimi/sessions/session-1", + updatedAt: 123, + }; + const target = { + id: "session-2", + workDir: root, + sessionDir: "/private/kimi/sessions/session-2", + updatedAt: 124, + }; + const runExclusiveAfterCancelling = vi.fn(async (action: () => Promise) => action()); + vi.spyOn(bridge.runtime, "getSession").mockReturnValue({ + runExclusiveAfterCancelling, + } as never); + host.harness.listSessions.mockResolvedValueOnce([source] as never); + host.harness.forkSession.mockResolvedValueOnce({ summary: target, close: vi.fn() }); + + const result = await bridge.handle( + { + id: "rpc-1", + method: Methods.ForkKimiSession, + params: { sessionId: "session-1", turnIndex: 0 }, + }, + "view-1", + ); + + expect(result).toEqual({ id: "rpc-1", result: { sessionId: "session-2" } }); + expect(runExclusiveAfterCancelling).toHaveBeenCalledOnce(); + expect(host.harness.forkSession).toHaveBeenCalledOnce(); + }); + + it("closes and removes a fork when its baseline cannot be materialized", async () => { + const source = { id: "session-1", workDir: root, updatedAt: 123 }; + const target = { id: "session-2", workDir: root, updatedAt: 124 }; + const close = vi.fn(async () => undefined); + host.harness.listSessions.mockResolvedValueOnce([source] as never); + host.harness.forkSession.mockResolvedValueOnce({ summary: target, close }); + vi.spyOn(bridge.baselineManager, "materializeToFork").mockRejectedValueOnce( + new Error("baseline unavailable"), + ); + const deleteBaseline = vi.spyOn(bridge.baselineManager, "deleteSession"); + + const result = await bridge.handle( + { + id: "rpc-1", + method: Methods.ForkKimiSession, + params: { sessionId: "session-1", turnIndex: 0 }, + }, + "view-1", + ); + + expect(result).toEqual({ id: "rpc-1", error: "baseline unavailable" }); + expect(close).toHaveBeenCalledOnce(); + expect(host.harness.deleteSession).toHaveBeenCalledWith("session-2"); + expect(deleteBaseline).toHaveBeenCalledWith("session-2"); + }); + + it("keeps conversation history available when its baseline snapshot disappears", async () => { + const session = createResumedSession("session-1", root); + host.harness.resumeSession.mockResolvedValueOnce(session as never); + host.showWarningMessage.mockResolvedValueOnce("Show Logs"); + const sourcePath = join(root, "app.ts"); + await writeFile(sourcePath, "original\n", "utf-8"); + await bridge.baselineManager.capture(session.summary, sourcePath); + const baselinesRoot = join(root, "global-storage", "baselines"); + const [homeDirectory] = await readdir(baselinesRoot); + const [sessionDirectory] = await readdir(join(baselinesRoot, homeDirectory!)); + const snapshotsDirectory = join( + baselinesRoot, + homeDirectory!, + sessionDirectory!, + "snapshots", + ); + const [snapshot] = await readdir(snapshotsDirectory); + await rm(join(snapshotsDirectory, snapshot!)); + + const result = await bridge.handle( + { + id: "rpc-1", + method: Methods.LoadKimiSessionHistory, + params: { kimiSessionId: "session-1" }, + }, + "view-1", + ); + + expect(result).toEqual({ + id: "rpc-1", + result: expect.arrayContaining([ + expect.objectContaining({ type: "StatusUpdate", _sessionId: "session-1" }), + ]), + }); + expect(writeLog).toHaveBeenCalledWith( + expect.stringMatching(/Unable to restore session file changes.*Unable to read baseline snapshot/), + ); + await vi.waitFor(() => expect(showLogs).toHaveBeenCalledOnce()); + }); + + it("returns a readable error when persisted session state is corrupt without wedging the bridge", async () => { + host.harness.resumeSession.mockRejectedValueOnce( + new Error("Session state is invalid JSON at line 4"), + ); + + const failed = await bridge.handle( + { + id: "rpc-1", + method: Methods.LoadKimiSessionHistory, + params: { kimiSessionId: "session-1" }, + }, + "view-1", + ); + const next = await bridge.handle({ id: "rpc-2", method: Methods.ShowLogs }, "view-1"); + + expect(failed).toEqual({ + id: "rpc-1", + error: "Session state is invalid JSON at line 4", + }); + expect(writeLog).toHaveBeenCalledWith( + expect.stringContaining("Session state is invalid JSON at line 4"), + ); + expect(next).toEqual({ id: "rpc-2", result: { ok: true } }); + }); +}); + +describe("Registered working directories", () => { + it("lists the workspace root when there is no session history", async () => { + host.harness.listSessions.mockResolvedValueOnce([] as never); + + const result = await bridge.handle({ id: "rpc-1", method: Methods.GetRegisteredWorkDirs }, "view-1"); + + expect(result).toEqual({ id: "rpc-1", result: [root] }); + }); + + it("keeps the selected working directory visible without session history", async () => { + const sub = join(root, "packages", "demo"); + await mkdir(sub, { recursive: true }); + host.harness.listSessions.mockResolvedValue([] as never); + + await bridge.handle({ id: "rpc-1", method: Methods.SetWorkDir, params: { workDir: sub } }, "view-1"); + const result = await bridge.handle({ id: "rpc-2", method: Methods.GetRegisteredWorkDirs }, "view-1"); + + expect(result).toEqual({ id: "rpc-2", result: [root, sub].toSorted() }); + }); + + it("merges session-history directories and hides directories outside the workspace", async () => { + const inside = join(root, "nested"); + host.harness.listSessions.mockResolvedValueOnce([ + { id: "s-1", workDir: inside }, + { id: "s-2", workDir: "/private/outside" }, + { id: "s-3", workDir: root }, + ] as never); + + const result = await bridge.handle({ id: "rpc-1", method: Methods.GetRegisteredWorkDirs }, "view-1"); + + expect(result).toEqual({ id: "rpc-1", result: [inside, root].toSorted() }); + expect(JSON.stringify(result)).not.toContain("/private/outside"); + }); +}); + +describe("Webview config saves (thinking effort persistence parity with the TUI)", () => { + const effortModel = { + provider: "managed:kimi-code", + model: "reasoning", + supportEfforts: ["low", "high", "max"], + defaultEffort: "high", + }; + + function mockConfig(thinking?: { enabled: boolean; effort?: string }) { + host.harness.getConfig.mockResolvedValue({ + defaultModel: "kimi/reasoning", + thinking, + models: { "kimi/reasoning": effortModel }, + } as never); + } + + it("persists a non-top effort as the global default", async () => { + mockConfig(); + + const result = await bridge.handle( + { id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "high" } }, + "view-1", + ); + + expect(result).toEqual({ id: "rpc-1", result: { ok: true } }); + expect(host.harness.setConfig).toHaveBeenCalledWith({ + defaultModel: "kimi/reasoning", + thinking: { enabled: true, effort: "high" }, + }); + }); + + it("keeps a pick above the model's delivered default session-only", async () => { + mockConfig(); + + await bridge.handle( + { id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "max" } }, + "view-1", + ); + + expect(host.harness.setConfig).toHaveBeenCalledWith({ + defaultModel: "kimi/reasoning", + thinking: { enabled: true }, + }); + }); + + it("persists the top tier when the model's delivered default is the top tier", async () => { + host.harness.getConfig.mockResolvedValue({ + defaultModel: "kimi/reasoning", + models: { "kimi/reasoning": { ...effortModel, defaultEffort: "max" } }, + } as never); + + await bridge.handle( + { id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "max" } }, + "view-1", + ); + + expect(host.harness.setConfig).toHaveBeenCalledWith({ + defaultModel: "kimi/reasoning", + thinking: { enabled: true, effort: "max" }, + }); + }); + + it("keeps an xhigh pick session-only when the default comes from the Anthropic profile inference", async () => { + // claude-opus-4-7 declares no efforts; the profile inference supplies + // [low, medium, high, xhigh, max] and resolves the default to "high". + host.harness.getConfig.mockResolvedValue({ + defaultModel: "custom/claude", + models: { "custom/claude": { provider: "custom", model: "claude-opus-4-7" } }, + } as never); + + await bridge.handle( + { id: "rpc-1", method: Methods.SaveConfig, params: { model: "custom/claude", thinking: true, effort: "xhigh" } }, + "view-1", + ); + + expect(host.harness.setConfig).toHaveBeenCalledWith({ + defaultModel: "custom/claude", + thinking: { enabled: true }, + }); + }); + + it("persists the concrete effort when the model's levels are unknown", async () => { + host.harness.getConfig.mockResolvedValue({ defaultModel: "other/model", models: {} }); + + await bridge.handle( + { id: "rpc-1", method: Methods.SaveConfig, params: { model: "custom/model", thinking: true, effort: "max" } }, + "view-1", + ); + + expect(host.harness.setConfig).toHaveBeenCalledWith({ + defaultModel: "custom/model", + thinking: { enabled: true, effort: "max" }, + }); + }); + + it("leaves the stored effort alone when the pick re-confirms the active effort", async () => { + mockConfig({ enabled: false, effort: "low" }); + + await bridge.handle( + { id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "high", effortChanged: false } }, + "view-1", + ); + + expect(host.harness.setConfig).toHaveBeenCalledWith({ + defaultModel: "kimi/reasoning", + thinking: { enabled: true }, + }); + }); + + it("skips the config write entirely when nothing changed", async () => { + mockConfig({ enabled: true, effort: "high" }); + + await bridge.handle( + { id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "high" } }, + "view-1", + ); + + expect(host.harness.setConfig).not.toHaveBeenCalled(); + }); +}); + +function createResumedSession(id: string, workDir: string) { + const close = vi.fn(async () => undefined); + const summary = { + id, + workDir, + sessionDir: join("/private/kimi/sessions", id), + createdAt: 1, + updatedAt: 2, + metadata: { vscode_legacy_approval: { yolo: false, afk: false } }, + }; + return { + id, + workDir, + summary, + close, + getResumeState: () => ({ + sessionMetadata: { agents: {} }, + agents: { + main: { + type: "main", + config: { + cwd: workDir, + modelAlias: "test-model", + modelCapabilities: { + image_in: false, + video_in: false, + audio_in: false, + thinking: false, + tool_use: true, + max_context_tokens: 128_000, + }, + thinkingEffort: "off", + systemPrompt: "", + }, + context: { history: [], tokenCount: 0 }, + replay: [], + permission: { mode: "manual", rules: [] }, + plan: null, + usage: {}, + tools: [], + background: [], + }, + }, + }), + getStatus: async () => ({ permission: "manual" }), + setPermission: async () => undefined, + updateMetadata: async () => undefined, + setApprovalHandler: () => undefined, + setQuestionHandler: () => undefined, + onEvent: () => () => undefined, + }; +} diff --git a/apps/vscode/test/event-adapter.test.ts b/apps/vscode/test/event-adapter.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc40ae541fa6cdf3422710e99fac1a9f105f202f --- /dev/null +++ b/apps/vscode/test/event-adapter.test.ts @@ -0,0 +1,570 @@ +/** + * Scenario: public Node SDK events are projected into the released VS Code Webview protocol. + * Responsibilities: verify legacy shapes, routing state, and terminal metadata one event at a time. + * Wiring: the pure adapter and real protocol types are used directly; there are no stubs. + * Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts apps/vscode/test/event-adapter.test.ts + */ + +import { describe, expect, it } from 'vitest'; + +import { isPreflightError } from '../shared/errors'; +import { + adaptSdkEvent, + createEventAdapterState, +} from '../src/runtime/event-adapter'; + +describe('event adapter (projects SDK events into the legacy Webview contract)', () => { + it('emits the pending input when a main-agent turn starts', () => { + const result = adaptSdkEvent( + createEventAdapterState(), + { + type: 'turn.started', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + origin: { kind: 'user' }, + }, + { pendingInput: 'Fix the failing test' }, + ); + + expect(result.event).toEqual({ + type: 'TurnBegin', + payload: { user_input: 'Fix the failing test' }, + _sessionId: 'session-1', + }); + }); + + it('emits text content when the assistant streams a delta', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'assistant.delta', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + delta: 'Done', + }); + + expect(result.event).toEqual({ + type: 'ContentPart', + payload: { type: 'text', text: 'Done' }, + _sessionId: 'session-1', + }); + }); + + it('emits thinking content when the model streams a thinking delta', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'thinking.delta', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + delta: 'Checking the types', + }); + + expect(result.event).toEqual({ + type: 'ContentPart', + payload: { type: 'think', think: 'Checking the types' }, + _sessionId: 'session-1', + }); + }); + + it('emits a numbered legacy step when an SDK step starts', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'turn.step.started', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + step: 2, + stepId: 'step-2', + }); + + expect(result.event).toEqual({ + type: 'StepBegin', + payload: { n: 2 }, + _sessionId: 'session-1', + }); + }); + + it.each([ + ['Bash', 'Shell'], + ['Read', 'ReadFile'], + ['Write', 'WriteFile'], + ['Edit', 'StrReplaceFile'], + ['TodoList', 'SetTodoList'], + ['Glob', 'Glob'], + ] as const)('maps the %s tool name to %s when a tool starts', (sdkName, legacyName) => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'tool.call.started', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'tool-1', + name: sdkName, + args: { path: 'src/index.ts' }, + }); + + expect(result.event).toEqual({ + type: 'ToolCall', + payload: { + type: 'function', + id: 'tool-1', + function: { + name: legacyName, + arguments: '{"path":"src/index.ts"}', + }, + }, + _sessionId: 'session-1', + }); + }); + + it('preserves each tool-call ID when argument deltas are interleaved', () => { + const state = createEventAdapterState(); + const first = adaptSdkEvent(state, { + type: 'tool.call.delta', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'tool-a', + name: 'Read', + argumentsPart: '{"path":"a', + }); + const second = adaptSdkEvent(first.state, { + type: 'tool.call.delta', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'tool-b', + name: 'Read', + argumentsPart: '{"path":"b', + }); + + expect([first.event, second.event]).toEqual([ + { + type: 'ToolCallPart', + payload: { tool_call_id: 'tool-a', arguments_part: '{"path":"a' }, + _sessionId: 'session-1', + }, + { + type: 'ToolCallPart', + payload: { tool_call_id: 'tool-b', arguments_part: '{"path":"b' }, + _sessionId: 'session-1', + }, + ]); + }); + + it('emits a legacy result when an SDK tool call finishes', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'tool.result', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'tool-1', + output: { exitCode: 0 }, + isError: false, + }); + + expect(result.event).toEqual({ + type: 'ToolResult', + payload: { + tool_call_id: 'tool-1', + return_value: { + is_error: false, + output: '{\n "exitCode": 0\n}', + message: '', + display: [], + }, + }, + _sessionId: 'session-1', + }); + }); + + it('carries a file diff display from tool start into its matching result', () => { + const started = adaptSdkEvent(createEventAdapterState(), { + type: 'tool.call.started', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'tool-1', + name: 'Edit', + args: { path: 'src/index.ts' }, + display: { + kind: 'diff', + path: 'src/index.ts', + before: 'old', + after: 'new', + }, + }); + const finished = adaptSdkEvent(started.state, { + type: 'tool.result', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'tool-1', + output: 'updated', + }); + + expect(finished.event).toMatchObject({ + type: 'ToolResult', + payload: { + return_value: { + display: [{ + type: 'diff', + path: 'src/index.ts', + old_text: 'old', + new_text: 'new', + }], + }, + }, + }); + }); + + it('carries a todo display only to the result with the same tool-call ID', () => { + const started = adaptSdkEvent(createEventAdapterState(), { + type: 'tool.call.started', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'todo-1', + name: 'TodoList', + args: {}, + display: { + kind: 'todo_list', + items: [{ title: 'Ship it', status: 'done' }], + }, + }); + const unrelated = adaptSdkEvent(started.state, { + type: 'tool.result', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'other', + output: 'other result', + }); + + expect(unrelated.event).toMatchObject({ + payload: { return_value: { display: [] } }, + }); + expect(unrelated.state.toolDisplays['todo-1']).toEqual([ + { type: 'todo', items: [{ title: 'Ship it', status: 'done' }] }, + ]); + const matching = adaptSdkEvent(unrelated.state, { + type: 'tool.result', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + toolCallId: 'todo-1', + output: 'updated', + }); + expect(matching.event).toMatchObject({ + payload: { + return_value: { + display: [{ type: 'todo', items: [{ title: 'Ship it', status: 'done' }] }], + }, + }, + }); + }); + + it('emits snake-case status fields when agent status changes', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + contextUsage: 0.25, + planMode: true, + usage: { + currentTurn: { + inputOther: 10, + output: 4, + inputCacheRead: 3, + inputCacheCreation: 2, + }, + }, + }); + + expect(result.event).toEqual({ + type: 'StatusUpdate', + payload: { + context_usage: 0.25, + plan_mode: true, + token_usage: { + input_other: 10, + output: 4, + input_cache_read: 3, + input_cache_creation: 2, + }, + }, + _sessionId: 'session-1', + }); + }); + + it('derives context_usage from the v2 context token pair', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + contextTokens: 25_600, + maxContextTokens: 256_000, + }); + + expect(result.event).toEqual({ + type: 'StatusUpdate', + payload: { context_usage: 0.1 }, + _sessionId: 'session-1', + }); + }); + + it('preserves an explicit contextUsage field over the context token pair', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + contextUsage: 0, + contextTokens: 25_600, + maxContextTokens: 256_000, + }); + + expect(result.event).toMatchObject({ + type: 'StatusUpdate', + payload: { context_usage: 0 }, + }); + }); + + it('does not derive a ratio from an invalid context capacity', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + contextTokens: 25_600, + maxContextTokens: 0, + }); + + expect(result.event).toBeUndefined(); + }); + + it('emits only new token usage when SDK status carries cumulative turn usage', () => { + const first = adaptSdkEvent(createEventAdapterState(), { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + usage: { + currentTurn: { + inputOther: 10, + output: 4, + inputCacheRead: 3, + inputCacheCreation: 2, + }, + }, + }); + const second = adaptSdkEvent(first.state, { + type: 'agent.status.updated', + sessionId: 'session-1', + agentId: 'main', + usage: { + currentTurn: { + inputOther: 14, + output: 7, + inputCacheRead: 8, + inputCacheCreation: 2, + }, + }, + }); + + expect(second.event).toEqual({ + type: 'StatusUpdate', + payload: { + token_usage: { + input_other: 4, + output: 3, + input_cache_read: 5, + input_cache_creation: 0, + }, + }, + _sessionId: 'session-1', + }); + }); + + it('routes a child-agent event through its parent tool after spawn', () => { + const spawned = adaptSdkEvent(createEventAdapterState(), { + type: 'subagent.spawned', + sessionId: 'session-1', + agentId: 'main', + subagentId: 'child-1', + subagentName: 'coder', + parentToolCallId: 'agent-call-1', + parentAgentId: 'main', + runInBackground: false, + }); + const childEvent = adaptSdkEvent(spawned.state, { + type: 'assistant.delta', + sessionId: 'session-1', + agentId: 'child-1', + turnId: 1, + delta: 'Child result', + }); + + expect(childEvent.event).toEqual({ + type: 'SubagentEvent', + payload: { + parent_tool_call_id: 'agent-call-1', + event: { + type: 'ContentPart', + payload: { type: 'text', text: 'Child result' }, + }, + }, + _sessionId: 'session-1', + }); + }); + + it('scopes a child tool-call ID when the child starts a tool', () => { + const spawned = adaptSdkEvent(createEventAdapterState(), { + type: 'subagent.spawned', + sessionId: 'session-1', + agentId: 'main', + subagentId: 'child-1', + subagentName: 'coder', + parentToolCallId: 'agent-call-1', + parentAgentId: 'main', + runInBackground: false, + }); + const childEvent = adaptSdkEvent(spawned.state, { + type: 'tool.call.started', + sessionId: 'session-1', + agentId: 'child-1', + turnId: 1, + toolCallId: 'tool-1', + name: 'Read', + args: { path: 'README.md' }, + }); + + expect(childEvent.event).toEqual({ + type: 'SubagentEvent', + payload: { + parent_tool_call_id: 'agent-call-1', + event: { + type: 'ToolCall', + payload: { + type: 'function', + id: 'child-1:tool-1', + function: { + name: 'ReadFile', + arguments: '{"path":"README.md"}', + }, + }, + }, + }, + _sessionId: 'session-1', + }); + }); + + it('emits compaction begin when SDK compaction starts', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'compaction.started', + sessionId: 'session-1', + agentId: 'main', + trigger: 'manual', + }); + + expect(result.event).toEqual({ + type: 'CompactionBegin', + payload: {}, + _sessionId: 'session-1', + }); + }); + + it.each([ + { + type: 'compaction.completed' as const, + result: { summary: 'Summary', compactedCount: 3, tokensBefore: 100, tokensAfter: 30 }, + }, + { type: 'compaction.cancelled' as const }, + { type: 'compaction.blocked' as const, turnId: 7 }, + ])('emits compaction end when SDK reports $type', (event) => { + const result = adaptSdkEvent(createEventAdapterState(), { + ...event, + sessionId: 'session-1', + agentId: 'main', + }); + + expect(result.event).toEqual({ + type: 'CompactionEnd', + payload: {}, + _sessionId: 'session-1', + }); + }); + + it('returns terminal metadata when the main turn completes', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'turn.ended', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + reason: 'completed', + durationMs: 50, + }); + + expect(result.event).toBeUndefined(); + expect(result.terminal).toEqual({ + key: 'session-1:main:7', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + reason: 'completed', + error: undefined, + }); + }); + + it('preserves the SDK error when a main turn fails', () => { + const result = adaptSdkEvent(createEventAdapterState(), { + type: 'turn.ended', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + reason: 'failed', + error: { + code: 'internal', + message: 'Provider failed', + retryable: false, + }, + }); + + expect(result.terminal).toEqual({ + key: 'session-1:main:7', + sessionId: 'session-1', + agentId: 'main', + turnId: 7, + reason: 'failed', + error: { + code: 'internal', + message: 'Provider failed', + retryable: false, + }, + }); + }); + + it('emits a bridge error with the caller-selected phase when the SDK reports an error', () => { + const result = adaptSdkEvent( + createEventAdapterState(), + { + type: 'error', + sessionId: 'session-1', + agentId: 'main', + code: 'internal', + message: 'Configuration failed', + details: { path: 'config.toml' }, + retryable: false, + }, + { errorPhase: 'preflight' }, + ); + + expect(result.event).toEqual({ + type: 'error', + code: 'internal', + message: 'Configuration failed', + detail: '{\n "path": "config.toml"\n}', + phase: 'preflight', + _sessionId: 'session-1', + }); + }); + + it('classifies a missing Windows Git Bash runtime as a preflight error', () => { + expect(isPreflightError('shell.git_bash_not_found')).toBe(true); + }); +}); diff --git a/apps/vscode/test/extension-host-smoke.test.ts b/apps/vscode/test/extension-host-smoke.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4467746355b2a90e1dc8447d680b5283d6a61dd1 --- /dev/null +++ b/apps/vscode/test/extension-host-smoke.test.ts @@ -0,0 +1,105 @@ +/** + * Scenario: maintainers run the installed-VSIX smoke from a developer machine or CI. + * Responsibilities: stable downloads cannot reuse stale caches, and the Extension + * Host cannot discover the developer's real legacy Kimi home. + * Wiring: real smoke orchestration and filesystem; @vscode/test-electron is the + * external process/download boundary. + * Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/extension-host-smoke.test.ts + */ +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const vscodeTest = vi.hoisted(() => ({ + runTests: vi.fn(), + runVSCodeCommand: vi.fn(), +})); + +vi.mock('@vscode/test-electron', () => vscodeTest); + +const { runExtensionHostSmoke } = await import('../scripts/extension-host-smoke.mjs'); +const tempDirs: string[] = []; + +beforeEach(() => { + vscodeTest.runVSCodeCommand.mockResolvedValue({ + stdout: 'Extension was successfully installed.\n', + stderr: '', + }); + vscodeTest.runTests.mockImplementation(async (options) => { + await writeFile( + options.extensionTestsEnv.KIMI_VSCODE_SMOKE_REPORT, + JSON.stringify({ vscode: options.version === 'stable' ? '1.127.0' : options.version }), + 'utf8', + ); + }); +}); + +afterEach(async () => { + vi.clearAllMocks(); + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe('installed VSIX Extension Host smoke', () => { + it('uses a fresh disposable download cache for every stable run', async () => { + const fixture = await makeFixture(); + + const first = await runExtensionHostSmoke({ + version: 'stable', + vsixPath: fixture.vsixPath, + cachePath: fixture.cachePath, + }); + const second = await runExtensionHostSmoke({ + version: 'stable', + vsixPath: fixture.vsixPath, + cachePath: fixture.cachePath, + }); + + expect(first.cachePath).not.toBe(second.cachePath); + expect(first.vscodeVersion).toBe('1.127.0'); + expect(second.vscodeVersion).toBe('1.127.0'); + }); + + it('gives the harness separate Kimi and operating-system homes before activation', async () => { + const fixture = await makeFixture(); + + await runExtensionHostSmoke({ + version: '1.100.0', + vsixPath: fixture.vsixPath, + cachePath: fixture.cachePath, + }); + + const options = vscodeTest.runTests.mock.calls[0]?.[0]; + const env = options.extensionTestsEnv; + expect(env.KIMI_CODE_HOME).not.toBe(env.KIMI_VSCODE_SMOKE_OS_HOME); + expect(env.KIMI_VSCODE_SMOKE_OS_HOME).toContain('os-home'); + }); + + it('rejects a cached host that does not match an exact requested version', async () => { + const fixture = await makeFixture(); + vscodeTest.runTests.mockImplementationOnce(async (options) => { + await writeFile( + options.extensionTestsEnv.KIMI_VSCODE_SMOKE_REPORT, + JSON.stringify({ vscode: '1.99.3' }), + 'utf8', + ); + }); + + await expect(runExtensionHostSmoke({ + version: '1.100.0', + vsixPath: fixture.vsixPath, + cachePath: fixture.cachePath, + })).rejects.toThrow('Extension Host ran VS Code 1.99.3, expected requested version 1.100.0'); + }); +}); + +async function makeFixture(): Promise<{ vsixPath: string; cachePath: string }> { + const root = await mkdtemp(join(tmpdir(), 'kimi-extension-host-smoke-')); + tempDirs.push(root); + const cachePath = join(root, 'cache'); + const vsixPath = join(root, 'kimi-code-test.vsix'); + await mkdir(cachePath, { recursive: true }); + await writeFile(vsixPath, 'fixture', 'utf8'); + return { vsixPath, cachePath }; +} diff --git a/apps/vscode/test/fork-turn-index.test.ts b/apps/vscode/test/fork-turn-index.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bfa5bb395eda5e0239c8f487320d2897fea60b8 --- /dev/null +++ b/apps/vscode/test/fork-turn-index.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; + +import { getForkTurnIndex } from "../shared/fork-turn-index"; + +interface TestMessage { + readonly id: string; + readonly role: "user" | "assistant"; + readonly content: string; + readonly timestamp: number; + readonly forkable?: boolean; + readonly steps?: readonly { + readonly n: number; + readonly items: readonly { readonly type: string; readonly content?: string }[]; + }[]; +} + +function message( + role: TestMessage["role"], + options: Partial = {}, +): TestMessage { + return { + id: crypto.randomUUID(), + role, + content: "", + timestamp: 1, + ...options, + }; +} + +describe("fork turn index", () => { + it("counts a steer embedded in the current assistant response", () => { + const messages = [ + message("user"), + message("assistant", { + steps: [{ n: 1, items: [{ type: "steer", content: "also fix tests" }] }], + }), + ]; + + expect(getForkTurnIndex(messages, 1)).toBe(1); + }); + + it("carries prior steer turns into later assistant responses", () => { + const messages = [ + message("user"), + message("assistant", { + steps: [{ n: 1, items: [{ type: "steer", content: "also fix tests" }] }], + }), + message("user"), + message("assistant"), + ]; + + expect(getForkTurnIndex(messages, 3)).toBe(2); + }); + + it("does not count or offer forks for host-only command output", () => { + const messages = [ + message("user", { content: "/compact", forkable: false }), + message("assistant", { content: "The context has been compacted.", forkable: false }), + message("user"), + message("assistant"), + ]; + + expect(getForkTurnIndex(messages, 1)).toBeUndefined(); + expect(getForkTurnIndex(messages, 3)).toBe(0); + }); +}); diff --git a/apps/vscode/test/kimi-harness.integration.test.ts b/apps/vscode/test/kimi-harness.integration.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..af5a3a3cc01d111b25b4072fb58e523e9105ad18 --- /dev/null +++ b/apps/vscode/test/kimi-harness.integration.test.ts @@ -0,0 +1,1447 @@ +/** + * Scenario: the VS Code host and another Node SDK client share one in-process Kimi home. + * Responsibilities: outbound host identity, config/session interoperability, MCP credential/edit compatibility, and terminal provider failures. + * Wiring: KimiRuntime, KimiHarness, core, storage, and HTTP provider adapter are real; only the remote provider is local. + * Run: pnpm --filter kimi-code exec vitest run test/kimi-harness.integration.test.ts + */ + +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + createKimiHarness, + type KimiHarness, +} from "@moonshot-ai/kimi-code-sdk"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("vscode", () => ({ + Uri: { file: (path: string) => ({ fsPath: path }) }, + window: { + showInformationMessage: async () => undefined, + showWarningMessage: async () => undefined, + showTextDocument: async () => undefined, + }, + workspace: { + getConfiguration: () => ({ get: (_key: string, fallback: unknown) => fallback }), + }, +})); + +import { + createFakeProviderHarness, + type FakeProviderHarness, +} from "../../../packages/kosong/test/e2e/fake-provider-harness"; +import { Events, Methods } from "../shared/bridge"; +import { + MCP_SECRET_MASK, + type MCPServerConfig, + type UpdateMCPServerRequest, +} from "../shared/legacy-sdk"; +import { configHandlers } from "../src/handlers/config.handler"; +import { chatHandlers } from "../src/handlers/chat.handler"; +import { mcpHandlers } from "../src/handlers/mcp.handler"; +import { parseHostSlashCommand, runHostSlashCommand } from "../src/handlers/slash-command"; +import type { HandlerContext } from "../src/handlers/types"; +import { KimiRuntime } from "../src/runtime/kimi-runtime"; +import type { SessionRuntime } from "../src/runtime/session-runtime"; + +const MODEL_ALIAS = "vscode-test"; +const PROVIDER_TOKEN = "sk-vscode-boundary-secret"; + +interface BroadcastRecord { + readonly event: string; + readonly data: unknown; + readonly webviewId?: string; +} + +interface LogRecord { + readonly message: string; + readonly error?: unknown; +} + +interface RuntimeRig { + readonly homeDir: string; + readonly workDir: string; + readonly provider: FakeProviderHarness; + readonly runtime: KimiRuntime; + readonly broadcasts: BroadcastRecord[]; + readonly logs: LogRecord[]; + readonly version: string; + closeProvider(): Promise; +} + +interface McpHandlerRig { + readonly homeDir: string; + readonly harness: KimiHarness; + readonly broadcasts: BroadcastRecord[]; + readonly logs: LogRecord[]; +} + +const cleanups: Array<() => Promise> = []; + +afterEach(async () => { + while (cleanups.length > 0) { + await cleanups.pop()?.(); + } +}); + +async function createRuntimeRig(extraAliases: readonly string[] = []): Promise { + const rootDir = await mkdtemp(join(tmpdir(), "kimi-vscode-harness-")); + const homeDir = join(rootDir, "home"); + const workDir = join(rootDir, "workspace"); + await Promise.all([mkdir(homeDir), mkdir(workDir)]); + + const provider = await createFakeProviderHarness(); + let providerOpen = true; + const closeProvider = async (): Promise => { + if (!providerOpen) return; + providerOpen = false; + await provider.close(); + }; + + await writeProviderConfig(homeDir, `${provider.baseUrl}/v1`, extraAliases); + const version = await readExtensionVersion(); + const broadcasts: BroadcastRecord[] = []; + const logs: LogRecord[] = []; + const runtime = new KimiRuntime({ + version, + homeDir, + broadcast: (event: string, data: unknown, webviewId?: string) => { + broadcasts.push({ event, data, webviewId }); + }, + captureBaseline: () => undefined, + log: (message, error) => { + logs.push({ message, error }); + }, + }); + + cleanups.push(async () => { + try { + await runtime.dispose(); + } finally { + try { + await closeProvider(); + } finally { + await rm(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } + } + }); + + return { + homeDir, + workDir, + provider, + runtime, + broadcasts, + logs, + closeProvider, + version, + }; +} + +async function createPlainHarness(homeDir: string): Promise { + const harness = createKimiHarness({ + homeDir, + identity: { productName: "kimi-code-cli", version: "test", platform: "kimi_code_cli" }, + }); + cleanups.push(() => harness.close()); + return harness; +} + +async function createMcpHandlerRig(): Promise { + const homeDir = await mkdtemp(join(tmpdir(), "kimi-vscode-mcp-handler-")); + cleanups.push(() => rm(homeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); + const harness = await createPlainHarness(homeDir); + const broadcasts: BroadcastRecord[] = []; + const logs: LogRecord[] = []; + return { homeDir, harness, broadcasts, logs }; +} + +async function updateMcpServer( + rig: McpHandlerRig, + request: UpdateMCPServerRequest | MCPServerConfig, +): Promise { + return mcpHandlers[Methods.UpdateMCPServer]!(request, mcpHandlerContext(rig)) as Promise; +} + +async function getMcpServers(rig: McpHandlerRig): Promise { + return mcpHandlers[Methods.GetMCPServers]!(undefined, mcpHandlerContext(rig)) as Promise; +} + +/** + * `harness.listMcpServers()` without the management-plane tags (`source` / + * `origin` / `mutable`) — these tests assert the stored config payload only. + */ +async function listStoredMcpServers(rig: McpHandlerRig): Promise { + return (await rig.harness.listMcpServers()).map( + ({ source: _source, origin: _origin, mutable: _mutable, ...entry }) => entry, + ); +} + +/** + * The Webview payload minus the management-plane tags — most handler tests + * assert the config payload only; the tags have their own passthrough test. + */ +function stripMcpTags(servers: MCPServerConfig[]): unknown[] { + return servers.map( + ({ source: _source, origin: _origin, mutable: _mutable, ...entry }) => entry, + ); +} + +function mcpHandlerContext(rig: McpHandlerRig): HandlerContext { + return { + harness: rig.harness, + broadcast: (event: string, data: unknown, webviewId?: string) => { + rig.broadcasts.push({ event, data, webviewId }); + }, + logError: (message: string, error: unknown) => { + rig.logs.push({ message, error }); + }, + } as unknown as HandlerContext; +} + +async function readExtensionVersion(): Promise { + const text = await readFile(new URL("../package.json", import.meta.url), "utf8"); + const parsed = JSON.parse(text) as { version?: unknown }; + if (typeof parsed.version !== "string") { + throw new TypeError("VS Code package version is missing"); + } + return parsed.version; +} + +async function writeProviderConfig( + homeDir: string, + baseUrl: string, + extraAliases: readonly string[] = [], +): Promise { + const extra = extraAliases + .map( + (alias) => ` +[models."${alias}"] +provider = "local" +model = "mock-model" +max_context_size = 128000 +capabilities = ["thinking"] +support_efforts = ["low", "high"] +`, + ) + .join("\n"); + await writeFile( + join(homeDir, "config.toml"), + `default_model = "${MODEL_ALIAS}" + +[providers.local] +type = "kimi" +base_url = "${baseUrl}" +api_key = "${PROVIDER_TOKEN}" + +[models."${MODEL_ALIAS}"] +provider = "local" +model = "mock-model" +max_context_size = 128000 +${extra} +[loop_control] +# The v1 engine reads max_retries_per_step; v2 renamed it to max_attempts_per_step. +max_retries_per_step = 1 +max_attempts_per_step = 1 +`, + "utf8", + ); +} + +function routeSuccessfulPrompt(provider: FakeProviderHarness): void { + provider.route("POST", "/v1/chat/completions", async (_request, reply) => { + await reply.sseJson(200, [ + completionChunk({ content: "mock response" }), + completionChunk({}, "stop"), + ]); + }); +} + +function routeBadRequest(provider: FakeProviderHarness): void { + provider.route("POST", "/v1/chat/completions", async (_request, reply) => { + await reply.json(400, { + error: { + message: "mock request rejected", + type: "invalid_request_error", + }, + }); + }); +} + +function routeBlockedPrompt(provider: FakeProviderHarness): { + readonly started: Promise; + readonly release: () => void; +} { + let markStarted!: () => void; + let release!: () => void; + const started = new Promise((resolve) => { + markStarted = resolve; + }); + const blocked = new Promise((resolve) => { + release = resolve; + }); + provider.route("POST", "/v1/chat/completions", async (_request, reply) => { + markStarted(); + await blocked; + await reply.sseJson(200, [completionChunk({ content: "late response" }), completionChunk({}, "stop")]); + }); + return { started, release }; +} + +function completionChunk( + delta: Record, + finishReason: string | null = null, +): Record { + return { + id: "chatcmpl-vscode-test", + object: "chat.completion.chunk", + created: 1, + model: "mock-model", + choices: [{ index: 0, delta, finish_reason: finishReason }], + }; +} + +async function openRuntimeSession(rig: RuntimeRig, sessionId?: string, yoloMode = false) { + return rig.runtime.openSession({ + webviewId: "view-1", + workDir: rig.workDir, + sessionId, + model: MODEL_ALIAS, + effort: "off", + yoloMode, + }); +} + +function streamChatContext(rig: RuntimeRig): HandlerContext { + return { + workDir: rig.workDir, + webviewId: "view-1", + broadcast: (event: string, data: unknown, webviewId?: string) => { + rig.broadcasts.push({ event, data, webviewId }); + }, + getOrCreateSession: async (model: string, effort: string, sessionId?: string) => + rig.runtime.openSession({ + webviewId: "view-1", + workDir: rig.workDir, + ...(sessionId === undefined ? {} : { sessionId }), + model, + effort, + yoloMode: false, + }), + getSession: () => rig.runtime.getSessionForView("view-1"), + saveAllDirty: async () => undefined, + logError: (message: string, error: unknown) => { + rig.logs.push({ message, error }); + }, + } as unknown as HandlerContext; +} + +function streamEvents(broadcasts: readonly BroadcastRecord[]): unknown[] { + return broadcasts + .filter((record) => record.event === Events.StreamEvent) + .map((record) => record.data); +} + +function diagnosticText(rig: RuntimeRig): string { + const logText = rig.logs + .map(({ message, error }) => { + const detail = error instanceof Error ? error.message : JSON.stringify(error ?? ""); + return `${message} ${detail}`; + }) + .join("\n"); + return `${logText}\n${JSON.stringify(streamEvents(rig.broadcasts))}`; +} + +async function runSlash( + runtime: SessionRuntime, + raw: string, + ctx = {} as HandlerContext, +): Promise { + const command = parseHostSlashCommand(raw); + if (command === undefined) throw new Error(`Expected host slash command: ${raw}`); + return runHostSlashCommand(runtime, command, ctx); +} + +describe("VS Code Kimi harness integration (shares one in-process SDK home)", () => { + it("only intercepts released slash commands and user-invoked skills", () => { + expect(parseHostSlashCommand("/plan on")).toEqual({ name: "plan", args: "on", raw: "/plan on" }); + expect(parseHostSlashCommand(" /skill:review carefully ")).toEqual({ + name: "skill:review", + args: "carefully", + raw: "/skill:review carefully", + }); + expect(parseHostSlashCommand("/not-a-host-command")).toBeUndefined(); + expect(parseHostSlashCommand([{ type: "text", text: "/clear" }])).toBeUndefined(); + }); + + it("combines the released slash commands with user-activatable workspace skills", async () => { + const commands = await configHandlers[Methods.GetSlashCommands]!(undefined, { + workDir: "/workspace", + harness: { + listWorkspaceSkills: async () => [ + { name: "review", description: "Review changes", path: "/skills/review", source: "user", type: "prompt" }, + { name: "reference-only", description: "Reference", path: "/skills/ref", source: "user", type: "reference" }, + ], + }, + logError: () => undefined, + } as unknown as HandlerContext); + + expect((commands as Array<{ name: string }>).map((command) => command.name)).toEqual([ + "init", + "compact", + "clear", + "yolo", + "auto", + "plan", + "add-dir", + "export", + "import", + "skill:review", + ]); + }); + + it("sends the package version in User-Agent when VS Code prompts the provider", async () => { + const rig = await createRuntimeRig(); + routeSuccessfulPrompt(rig.provider); + const session = await openRuntimeSession(rig); + + await expect(session.prompt("hello")).resolves.toEqual({ status: "finished" }); + + expect(rig.provider.requests[0]?.headers["user-agent"]).toBe( + `kimi-code-vscode/${rig.version}`, + ); + }); + + it("reloads sequential config writes from either harness sharing one home", async () => { + const rig = await createRuntimeRig(); + const plain = await createPlainHarness(rig.homeDir); + + await plain.setConfig({ thinking: { enabled: true, effort: "high" } }); + await expect(rig.runtime.harness.getConfig({ reload: true })).resolves.toMatchObject({ + thinking: { enabled: true, effort: "high" }, + }); + + await rig.runtime.harness.setConfig({ yolo: true }); + await expect(plain.getConfig({ reload: true })).resolves.toMatchObject({ yolo: true }); + }); + + it("masks credential-valued MCP fields at the Webview list boundary while leaving ordinary values visible", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://example.test/mcp", + headers: { + Authorization: "Bearer header-secret", + Cookie: "session=cookie-secret", + "X-API-Key": "api-key-secret", + "X-Workspace": "workspace-visible", + }, + }); + await rig.harness.addMcpServer({ + name: "local", + transport: "stdio", + command: "example-mcp", + env: { + SERVICE_TOKEN: "env-secret", + DEBUG: "debug-visible", + }, + }); + + const servers = await getMcpServers(rig); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "remote", + transport: "http", + url: "https://example.test/mcp", + headers: { + Authorization: MCP_SECRET_MASK, + Cookie: MCP_SECRET_MASK, + "X-API-Key": MCP_SECRET_MASK, + "X-Workspace": "workspace-visible", + }, + }, + { + name: "local", + transport: "stdio", + command: "example-mcp", + env: { + SERVICE_TOKEN: MCP_SECRET_MASK, + DEBUG: "debug-visible", + }, + }, + ]); + expect(JSON.stringify(servers)).not.toMatch(/header-secret|cookie-secret|api-key-secret|env-secret/); + }); + + it("passes the management-plane tags through to the Webview payload", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://example.test/mcp", + }); + + const servers = await getMcpServers(rig); + + expect(servers).toEqual([ + { + name: "remote", + transport: "http", + url: "https://example.test/mcp", + source: "global", + origin: join(rig.homeDir, "mcp.json"), + mutable: true, + }, + ]); + }); + + it("keeps project-layer servers in the list refreshed after every mutation", async () => { + const rig = await createMcpHandlerRig(); + const project = await mkdtemp(join(tmpdir(), "kimi-vscode-mcp-project-")); + cleanups.push(() => rm(project, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })); + await mkdir(join(project, ".git"), { recursive: true }); + await writeFile( + join(project, ".mcp.json"), + JSON.stringify({ + mcpServers: { "project-api": { transport: "http", url: "https://example.test/project" } }, + }), + ); + const ctx = { ...mcpHandlerContext(rig), workDir: project } as HandlerContext; + const call = (handler: string, params: unknown) => + mcpHandlers[handler]!(params, ctx) as Promise; + + await rig.harness.trustWorkspace(project); + + // The initial workspace-aware list shows the project entry as read-only, + // and every mutation's refreshed list keeps showing it (the mutation RPCs + // return a cwd-less list, so the handler must re-list with the workspace). + const assertList = (servers: MCPServerConfig[]): void => { + const projectEntry = servers.find((server) => server.name === "project-api"); + expect(projectEntry).toMatchObject({ mutable: false, url: "https://example.test/project" }); + }; + assertList(await call(Methods.GetMCPServers, undefined)); + + const added = await call(Methods.AddMCPServer, { + name: "user-api", + transport: "http", + url: "https://example.test/user", + }); + assertList(added); + assertList(rig.broadcasts.at(-1)!.data as MCPServerConfig[]); + + const removed = await call(Methods.RemoveMCPServer, { name: "user-api" }); + assertList(removed); + assertList(rig.broadcasts.at(-1)!.data as MCPServerConfig[]); + }); + + it("logs a failed MCP test without returning credential values to the Webview", async () => { + const rig = await createMcpHandlerRig(); + vi.spyOn(rig.harness, "testMcpServer").mockResolvedValue({ + success: false, + output: [ + "spawn missing-mcp ENOENT", + "Authorization: Bearer header-secret", + "TOKEN=env-secret", + "Cookie: session=cookie-secret", + ].join("\n"), + }); + + const result = await mcpHandlers[Methods.TestMCP]!( + { name: "broken" }, + mcpHandlerContext(rig), + ); + + expect(result).toMatchObject({ success: false, output: expect.stringContaining("ENOENT") }); + expect(JSON.stringify(result)).not.toMatch(/header-secret|env-secret|cookie-secret/); + expect(rig.logs).toHaveLength(1); + expect(rig.logs[0]?.message).toBe('MCP server test failed for "broken"'); + expect(rig.logs[0]?.error).toBeInstanceOf(Error); + expect((rig.logs[0]?.error as Error).message).toContain("ENOENT"); + expect((rig.logs[0]?.error as Error).message).not.toMatch(/header-secret|env-secret|cookie-secret/); + }); + + it("preserves an unchanged masked HTTP credential without exposing it in the response or broadcast", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://old.example.test/mcp", + headers: { + Authorization: "Bearer stored-header-secret", + "X-Workspace": "old-workspace", + }, + }); + + const servers = await updateMcpServer(rig, { + originalName: "remote", + server: { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { + Authorization: MCP_SECRET_MASK, + "X-Workspace": "new-workspace", + }, + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { + Authorization: MCP_SECRET_MASK, + "X-Workspace": "new-workspace", + }, + }, + ]); + expect(rig.broadcasts).toEqual([ + { event: Events.MCPServersChanged, data: servers, webviewId: undefined }, + ]); + await expect(listStoredMcpServers(rig)).resolves.toEqual([ + { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { + Authorization: "Bearer stored-header-secret", + "X-Workspace": "new-workspace", + }, + }, + ]); + }); + + it("preserves an unchanged masked stdio credential in the host configuration", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "local", + transport: "stdio", + command: "old-command", + env: { + SERVICE_TOKEN: "stored-env-secret", + DEBUG: "old-debug", + }, + }); + + const servers = await updateMcpServer(rig, { + originalName: "local", + server: { + name: "local", + transport: "stdio", + command: "new-command", + env: { + SERVICE_TOKEN: MCP_SECRET_MASK, + DEBUG: "new-debug", + }, + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "local", + transport: "stdio", + command: "new-command", + env: { + SERVICE_TOKEN: MCP_SECRET_MASK, + DEBUG: "new-debug", + }, + }, + ]); + await expect(listStoredMcpServers(rig)).resolves.toEqual([ + { + name: "local", + transport: "stdio", + command: "new-command", + env: { + SERVICE_TOKEN: "stored-env-secret", + DEBUG: "new-debug", + }, + }, + ]); + }); + + it("replaces an HTTP credential when the Webview submits a new literal value", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://example.test/mcp", + headers: { Authorization: "Bearer old-secret" }, + }); + + const servers = await updateMcpServer(rig, { + originalName: "remote", + server: { + name: "remote", + transport: "http", + url: "https://example.test/mcp", + headers: { Authorization: "Bearer new-secret" }, + }, + }); + + expect(servers[0]?.headers).toEqual({ Authorization: MCP_SECRET_MASK }); + await expect(listStoredMcpServers(rig)).resolves.toEqual([ + { + name: "remote", + transport: "http", + url: "https://example.test/mcp", + headers: { Authorization: "Bearer new-secret" }, + }, + ]); + }); + + it("replaces a stdio credential when the Webview submits a new literal value", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "local", + transport: "stdio", + command: "example-mcp", + env: { SERVICE_TOKEN: "old-secret" }, + }); + + const servers = await updateMcpServer(rig, { + originalName: "local", + server: { + name: "local", + transport: "stdio", + command: "example-mcp", + env: { SERVICE_TOKEN: "new-secret" }, + }, + }); + + expect(servers[0]?.env).toEqual({ SERVICE_TOKEN: MCP_SECRET_MASK }); + await expect(listStoredMcpServers(rig)).resolves.toEqual([ + { + name: "local", + transport: "stdio", + command: "example-mcp", + env: { SERVICE_TOKEN: "new-secret" }, + }, + ]); + }); + + it("preserves existing HTTP MCP headers when the released form updates the server", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://old.example.test/mcp", + headers: { "X-Workspace": "kept" }, + bearerTokenEnvVar: "REMOTE_MCP_TOKEN", + }); + + await updateMcpServer(rig, { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + auth: "oauth", + }); + + await expect(listStoredMcpServers(rig)).resolves.toEqual([ + { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { "X-Workspace": "kept" }, + bearerTokenEnvVar: "REMOTE_MCP_TOKEN", + auth: "oauth", + }, + ]); + }); + + it("removes stored stdio arguments when the structured edit omits them", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "local", + transport: "stdio", + command: "old-command", + args: ["--old"], + env: { KEEP: "yes" }, + }); + + const servers = await updateMcpServer(rig, { + originalName: "local", + server: { + name: "local", + transport: "stdio", + command: "new-command", + env: { KEEP: "yes" }, + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "local", + transport: "stdio", + command: "new-command", + env: { KEEP: "yes" }, + }, + ]); + }); + + it("removes stored stdio environment variables when the structured edit omits them", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "local", + transport: "stdio", + command: "old-command", + args: ["--keep"], + env: { REMOVE_TOKEN: "stored-secret" }, + }); + + const servers = await updateMcpServer(rig, { + originalName: "local", + server: { + name: "local", + transport: "stdio", + command: "new-command", + args: ["--keep"], + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "local", + transport: "stdio", + command: "new-command", + args: ["--keep"], + }, + ]); + }); + + it("removes stored HTTP headers when the structured edit omits them", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://old.example.test/mcp", + headers: { Authorization: "Bearer stored-secret" }, + bearerTokenEnvVar: "KEEP_TOKEN", + auth: "oauth", + }); + + const servers = await updateMcpServer(rig, { + originalName: "remote", + server: { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + bearerTokenEnvVar: "KEEP_TOKEN", + auth: "oauth", + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + bearerTokenEnvVar: "KEEP_TOKEN", + auth: "oauth", + }, + ]); + }); + + it("removes the stored bearer token reference when the structured edit omits it", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://old.example.test/mcp", + headers: { "X-Keep": "yes" }, + bearerTokenEnvVar: "REMOVE_TOKEN", + auth: "oauth", + }); + + const servers = await updateMcpServer(rig, { + originalName: "remote", + server: { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { "X-Keep": "yes" }, + auth: "oauth", + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { "X-Keep": "yes" }, + auth: "oauth", + }, + ]); + }); + + it("switches an OAuth HTTP server back to ordinary HTTP when auth is omitted", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "remote", + transport: "http", + url: "https://old.example.test/mcp", + headers: { "X-Keep": "yes" }, + bearerTokenEnvVar: "KEEP_TOKEN", + auth: "oauth", + }); + + const servers = await updateMcpServer(rig, { + originalName: "remote", + server: { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { "X-Keep": "yes" }, + bearerTokenEnvVar: "KEEP_TOKEN", + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "remote", + transport: "http", + url: "https://new.example.test/mcp", + headers: { "X-Keep": "yes" }, + bearerTokenEnvVar: "KEEP_TOKEN", + }, + ]); + }); + + it("moves an edited server from its original name to the new name", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "old-name", + transport: "stdio", + command: "old-command", + env: { API_TOKEN: "stored-secret" }, + enabled: false, + }); + + const servers = await updateMcpServer(rig, { + originalName: "old-name", + server: { + name: "new-name", + transport: "stdio", + command: "new-command", + env: { API_TOKEN: MCP_SECRET_MASK }, + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "new-name", + transport: "stdio", + command: "new-command", + env: { API_TOKEN: MCP_SECRET_MASK }, + enabled: false, + }, + ]); + await expect(listStoredMcpServers(rig)).resolves.toEqual([ + { + name: "new-name", + transport: "stdio", + command: "new-command", + env: { API_TOKEN: "stored-secret" }, + enabled: false, + }, + ]); + }); + + it("preserves a Windows executable path containing spaces through a structured edit", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "windows", + transport: "stdio", + command: "old-command", + }); + + const servers = await updateMcpServer(rig, { + originalName: "windows", + server: { + name: "windows", + transport: "stdio", + command: "C:\\Program Files\\Example MCP\\server.exe", + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "windows", + transport: "stdio", + command: "C:\\Program Files\\Example MCP\\server.exe", + }, + ]); + }); + + it("preserves Windows arguments containing spaces through a structured edit", async () => { + const rig = await createMcpHandlerRig(); + await rig.harness.addMcpServer({ + name: "windows", + transport: "stdio", + command: "node.exe", + }); + + const servers = await updateMcpServer(rig, { + originalName: "windows", + server: { + name: "windows", + transport: "stdio", + command: "node.exe", + args: ["--config", "C:\\Users\\Example User\\mcp config.json", "literal value"], + }, + }); + + expect(stripMcpTags(servers)).toEqual([ + { + name: "windows", + transport: "stdio", + command: "node.exe", + args: ["--config", "C:\\Users\\Example User\\mcp config.json", "literal value"], + }, + ]); + }); + + it("lists a closed VS Code session from a plain harness", async () => { + const rig = await createRuntimeRig(); + const vscodeSession = await openRuntimeSession(rig); + await rig.runtime.detachView("view-1"); + const plain = await createPlainHarness(rig.homeDir); + + const listed = await plain.listSessions({ workDir: rig.workDir }); + + expect(listed).toContainEqual(expect.objectContaining({ id: vscodeSession.id })); + }); + + it("resumes a closed VS Code session from a plain harness", async () => { + const rig = await createRuntimeRig(); + const vscodeSession = await openRuntimeSession(rig); + await rig.runtime.detachView("view-1"); + const plain = await createPlainHarness(rig.homeDir); + + const resumed = await plain.resumeSession({ id: vscodeSession.id }); + + expect(resumed.id).toBe(vscodeSession.id); + }); + + it("lists a closed plain-harness session from VS Code", async () => { + const rig = await createRuntimeRig(); + const plain = await createPlainHarness(rig.homeDir); + const plainSession = await plain.createSession({ + id: "ses_plain_to_vscode", + workDir: rig.workDir, + model: MODEL_ALIAS, + }); + await plainSession.close(); + + const listed = await rig.runtime.harness.listSessions({ workDir: rig.workDir }); + + expect(listed).toContainEqual(expect.objectContaining({ id: plainSession.id })); + }); + + it("resumes a closed plain-harness session from VS Code", async () => { + const rig = await createRuntimeRig(); + const plain = await createPlainHarness(rig.homeDir); + const plainSession = await plain.createSession({ + id: "ses_plain_to_vscode", + workDir: rig.workDir, + model: MODEL_ALIAS, + }); + await plainSession.close(); + + const resumed = await openRuntimeSession(rig, plainSession.id); + + expect(resumed.id).toBe(plainSession.id); + }); + + it("backfills approval flags for a session migrated before the metadata field existed", async () => { + const rig = await createRuntimeRig(); + const legacySessionDir = join(rig.workDir, "legacy-session"); + await mkdir(legacySessionDir); + await writeFile( + join(legacySessionDir, "state.json"), + JSON.stringify({ approval: { yolo: false, afk: true } }), + "utf8", + ); + const plain = await createPlainHarness(rig.homeDir); + const migrated = await plain.createSession({ + id: "ses_preexisting_migration", + workDir: rig.workDir, + metadata: { kimi_cli_source_path: legacySessionDir }, + }); + await migrated.close(); + + const resumed = await openRuntimeSession(rig, migrated.id); + + expect(resumed.legacyApprovalFlags).toEqual({ yolo: false, afk: true }); + expect(resumed.summary?.metadata?.["vscode_legacy_approval"]).toEqual({ + yolo: false, + afk: true, + }); + }); + + it("reports corrupt legacy approval state and still opens the migrated session", async () => { + const rig = await createRuntimeRig(); + const legacySessionDir = join(rig.workDir, "corrupt-legacy-session"); + await mkdir(legacySessionDir); + await writeFile(join(legacySessionDir, "state.json"), "{not-json", "utf8"); + const plain = await createPlainHarness(rig.homeDir); + const migrated = await plain.createSession({ + id: "ses_corrupt_preexisting_migration", + workDir: rig.workDir, + metadata: { kimi_cli_source_path: legacySessionDir }, + }); + await migrated.close(); + + const resumed = await openRuntimeSession(rig, migrated.id); + + expect(resumed.legacyApprovalFlags).toEqual({ yolo: false, afk: false }); + expect(rig.logs).toContainEqual({ + message: "Unable to restore legacy session approval settings", + error: expect.any(SyntaxError), + }); + }); + + it("imports a UTF-8 text file into the same session without calling the model", async () => { + const rig = await createRuntimeRig(); + await writeFile(join(rig.workDir, "notes.md"), "Keep the public API stable.", "utf8"); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, "/import notes.md")).resolves.toBe(true); + + await expect(runtime.session.getContext()).resolves.toMatchObject({ + history: [ + { + role: "user", + content: expect.arrayContaining([ + expect.objectContaining({ + type: "text", + text: expect.stringContaining("Keep the public API stable."), + }), + ]), + }, + ], + }); + expect(rig.provider.requests).toHaveLength(0); + expect(streamEvents(rig.broadcasts)).toContainEqual({ + type: "TurnBegin", + payload: { user_input: "/import notes.md", forkable: true }, + _sessionId: runtime.id, + }); + }); + + it("clears imported context without replacing the current session", async () => { + const rig = await createRuntimeRig(); + const runtime = await openRuntimeSession(rig); + await runtime.session.importContext("Prior context.", "file 'prior.md'"); + const sessionId = runtime.id; + + await expect(runSlash(runtime, "/clear")).resolves.toBe(true); + + expect(runtime.id).toBe(sessionId); + await expect(runtime.session.getContext()).resolves.toEqual({ history: [], tokenCount: 0 }); + }); + + it("applies the composer-submitted model before the turn starts", async () => { + const rig = await createRuntimeRig(["vscode-alt"]); + routeSuccessfulPrompt(rig.provider); + const runtime = await openRuntimeSession(rig); + + const result = await chatHandlers[Methods.StreamChat]!( + { content: "hi", model: "vscode-alt", effort: "high" }, + streamChatContext(rig), + ); + + expect(result).toEqual({ done: true }); + await expect(runtime.session.getStatus()).resolves.toMatchObject({ + model: "vscode-alt", + thinkingEffort: "high", + }); + }); + + it("toggles plan mode through the public session without calling the model", async () => { + const rig = await createRuntimeRig(); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, "/plan on")).resolves.toBe(true); + await expect(runtime.session.getStatus()).resolves.toMatchObject({ planMode: true }); + await expect(runSlash(runtime, "/plan off")).resolves.toBe(true); + await expect(runtime.session.getStatus()).resolves.toMatchObject({ planMode: false }); + expect(rig.provider.requests).toHaveLength(0); + }); + + it("keeps a slash-added directory after VS Code closes and resumes the session", async () => { + const rig = await createRuntimeRig(); + const additionalDir = join(rig.workDir, "directory with spaces"); + await mkdir(additionalDir); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, `/add-dir "${additionalDir}"`)).resolves.toBe(true); + const sessionId = runtime.id; + await rig.runtime.detachView("view-1"); + const resumed = await openRuntimeSession(rig, sessionId); + + expect(resumed.session.summary?.additionalDirs).toContain(additionalDir); + }); + + it("rejects an invalid plan subcommand without leaving the runtime busy", async () => { + const rig = await createRuntimeRig(); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, "/plan sideways")).rejects.toThrow( + "Unknown plan subcommand: sideways", + ); + + expect(runtime.isBusy).toBe(false); + }); + + it("fails a prompt sent while a turn is running without disturbing the active turn", async () => { + const rig = await createRuntimeRig(); + const blocked = routeBlockedPrompt(rig.provider); + const runtime = await openRuntimeSession(rig); + const first = runtime.prompt("first message"); + await blocked.started; + + await expect(runtime.prompt("concurrent message")).resolves.toEqual({ status: "failed" }); + + // The rejection surfaces as a mid-turn warning; the active turn is untouched. + expect(runtime.isBusy).toBe(true); + expect(streamEvents(rig.broadcasts)).toContainEqual( + expect.objectContaining({ type: "error", terminal: false }), + ); + + blocked.release(); + await expect(first).resolves.toEqual({ status: "finished" }); + expect(runtime.isBusy).toBe(false); + }); + + it("stops a running init command without surfacing its late result", async () => { + const rig = await createRuntimeRig(); + const blocked = routeBlockedPrompt(rig.provider); + const runtime = await openRuntimeSession(rig); + const command = runSlash(runtime, "/init"); + await blocked.started; + + await runtime.cancel(); + blocked.release(); + + await expect(command).resolves.toBe(false); + expect(runtime.isBusy).toBe(false); + expect(JSON.stringify(streamEvents(rig.broadcasts))).not.toContain("late response"); + }); + + it("stops a running manual compaction through the compaction cancellation API", async () => { + const rig = await createRuntimeRig(); + const blocked = routeBlockedPrompt(rig.provider); + const runtime = await openRuntimeSession(rig); + await runtime.session.importContext("Enough prior context to compact.", "file 'prior.md'"); + const command = runSlash(runtime, "/compact keep decisions"); + await blocked.started; + + await runtime.cancel(); + blocked.release(); + + await expect(command).resolves.toBe(false); + expect(runtime.isBusy).toBe(false); + }); + + it("keeps the host action busy until manual compaction completes", async () => { + const rig = await createRuntimeRig(); + routeSuccessfulPrompt(rig.provider); + const runtime = await openRuntimeSession(rig); + await runtime.session.importContext("Enough prior context to compact.", "file 'prior.md'"); + + const command = runSlash(runtime, "/compact keep decisions"); + expect(runtime.isBusy).toBe(true); + + await expect(command).resolves.toBe(true); + expect(runtime.isBusy).toBe(false); + expect(streamEvents(rig.broadcasts)).toContainEqual({ + type: "CompactionEnd", + payload: {}, + _sessionId: runtime.id, + }); + }); + + it("keeps /yolo and /afk independent when they are combined", async () => { + const rig = await createRuntimeRig(); + const runtime = await openRuntimeSession(rig); + + await runSlash(runtime, "/yolo"); + expect(runtime.legacyApprovalFlags).toEqual({ yolo: true, afk: false }); + await expect(runtime.session.getStatus()).resolves.toMatchObject({ permission: "yolo" }); + + await runSlash(runtime, "/afk"); + expect(runtime.legacyApprovalFlags).toEqual({ yolo: true, afk: true }); + await expect(runtime.session.getStatus()).resolves.toMatchObject({ permission: "auto" }); + + await runSlash(runtime, "/afk"); + expect(runtime.legacyApprovalFlags).toEqual({ yolo: true, afk: false }); + await expect(runtime.session.getStatus()).resolves.toMatchObject({ permission: "yolo" }); + }); + + it("applies the global yolo setting when a closed VS Code session reopens", async () => { + const rig = await createRuntimeRig(); + const first = await openRuntimeSession(rig); + await runSlash(first, "/yolo"); + await rig.runtime.detachView("view-1"); + + const reopened = await openRuntimeSession(rig, first.id); + expect(reopened.legacyApprovalFlags).toEqual({ yolo: false, afk: false }); + await expect(reopened.session.getStatus()).resolves.toMatchObject({ permission: "manual" }); + await rig.runtime.detachView("view-1"); + + const yoloReopened = await openRuntimeSession(rig, first.id, true); + expect(yoloReopened.legacyApprovalFlags).toEqual({ yolo: true, afk: false }); + await expect(yoloReopened.session.getStatus()).resolves.toMatchObject({ permission: "yolo" }); + }); + + it("exports current context as Markdown under the workspace", async () => { + const rig = await createRuntimeRig(); + const runtime = await openRuntimeSession(rig); + await runtime.session.importContext("Prior context.", "file 'prior.md'"); + + await expect(runSlash(runtime, "/export exported.md")).resolves.toBe(true); + + const markdown = await readFile(join(rig.workDir, "exported.md"), "utf8"); + expect(markdown).toContain("# Kimi Session Export"); + expect(markdown).toContain("Prior context."); + }); + + it("releases the host action after an invalid import so another command can run", async () => { + const rig = await createRuntimeRig(); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, "/import missing.md", { + harness: rig.runtime.harness, + runtime: rig.runtime, + } as HandlerContext)).rejects.toThrow( + "is not a valid file path or session ID", + ); + + expect(runtime.isBusy).toBe(false); + await expect(runSlash(runtime, "/clear")).resolves.toBe(true); + }); + + it("rejects a non-text import without changing the session context", async () => { + const rig = await createRuntimeRig(); + await writeFile(join(rig.workDir, "archive.zip"), "not really a zip", "utf8"); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, "/import archive.zip")).rejects.toThrow( + "/import only supports text-based files", + ); + await expect(runtime.session.getContext()).resolves.toEqual({ history: [], tokenCount: 0 }); + }); + + it("rejects invalid UTF-8 import bytes with a readable error", async () => { + const rig = await createRuntimeRig(); + await writeFile(join(rig.workDir, "broken.txt"), Buffer.from([0xc3, 0x28])); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, "/import broken.txt")).rejects.toThrow( + "the file is not valid UTF-8 text", + ); + }); + + it("rejects an import larger than the public 10 MB limit", async () => { + const rig = await createRuntimeRig(); + await writeFile(join(rig.workDir, "large.txt"), Buffer.alloc(10 * 1024 * 1024 + 1, 0x61)); + const runtime = await openRuntimeSession(rig); + + await expect(runSlash(runtime, "/import large.txt")).rejects.toThrow( + "Maximum import size is 10 MB", + ); + }); + + it("reports an unwritable export path and leaves the runtime usable", async () => { + const rig = await createRuntimeRig(); + await writeFile(join(rig.workDir, "not-a-directory"), "blocking file", "utf8"); + const runtime = await openRuntimeSession(rig); + await runtime.session.importContext("Prior context.", "file 'prior.md'"); + + await expect(runSlash(runtime, "/export not-a-directory/export.md")).rejects.toThrow(); + + expect(runtime.isBusy).toBe(false); + await expect(runSlash(runtime, "/clear")).resolves.toBe(true); + }); + + it("settles the prompt as failed when the provider returns 400", async () => { + const rig = await createRuntimeRig(); + routeBadRequest(rig.provider); + const session = await openRuntimeSession(rig); + + await expect(session.prompt("reject this request")).resolves.toEqual({ status: "failed" }); + + expect(session.isBusy).toBe(false); + expect(streamEvents(rig.broadcasts)).toContainEqual( + expect.objectContaining({ type: "error", phase: "runtime" }), + ); + }); + + it("accepts a new prompt after a provider 400 ends the previous turn", async () => { + const rig = await createRuntimeRig(); + let calls = 0; + rig.provider.route("POST", "/v1/chat/completions", async (_request, reply) => { + calls += 1; + if (calls === 1) { + await reply.json(400, { + error: { message: "mock request rejected", type: "invalid_request_error" }, + }); + return; + } + await reply.sseJson(200, [ + completionChunk({ content: "recovered" }), + completionChunk({}, "stop"), + ]); + }); + const session = await openRuntimeSession(rig); + await session.prompt("first request"); + + await expect(session.prompt("second request")).resolves.toEqual({ status: "finished" }); + }); + + it("does not expose the provider token when reporting a provider 400", async () => { + const rig = await createRuntimeRig(); + routeBadRequest(rig.provider); + const session = await openRuntimeSession(rig); + + await session.prompt("reject this request"); + + expect(diagnosticText(rig)).not.toContain(PROVIDER_TOKEN); + }); + + it("keeps the provider's 400 detail in the extension-host log", async () => { + const rig = await createRuntimeRig(); + routeBadRequest(rig.provider); + const session = await openRuntimeSession(rig); + + await session.prompt("reject this request"); + + expect(rig.logs).toContainEqual(expect.objectContaining({ + message: "Session turn failed", + error: expect.objectContaining({ message: expect.stringContaining("mock request rejected") }), + })); + }); + + it("settles the prompt as failed when the provider connection is unavailable", async () => { + const rig = await createRuntimeRig(); + const session = await openRuntimeSession(rig); + await rig.closeProvider(); + + await expect(session.prompt("connection test")).resolves.toEqual({ status: "failed" }); + + expect(session.isBusy).toBe(false); + expect(streamEvents(rig.broadcasts)).toContainEqual( + expect.objectContaining({ + type: "error", + code: "provider.connection_error", + phase: "runtime", + }), + ); + }); +}); diff --git a/apps/vscode/test/kimi-runtime.test.ts b/apps/vscode/test/kimi-runtime.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..804a6c946ae97b1e81e34b1ff225e2d2a5f2ba07 --- /dev/null +++ b/apps/vscode/test/kimi-runtime.test.ts @@ -0,0 +1,813 @@ +/** + * Scenario: the VS Code host owns one Kimi harness and routes Webviews to shared SDK sessions. + * Responsibilities: create/resume/switch, per-session settings, multi-view ownership, detach, and disposal. + * Wiring: KimiRuntime and SessionRuntime are real; in-memory Session/KimiHarness fakes form the public SDK boundary. + * Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts test/kimi-runtime.test.ts + */ + +import type { + ApprovalHandler, + CreateSessionOptions, + Event, + JsonObject, + KimiHarness, + PermissionMode, + PromptInput, + QuestionHandler, + ResumeSessionInput, + Session, + SessionStatus, + SessionSummary, + ThinkingEffort, +} from "@moonshot-ai/kimi-code-sdk"; +import { describe, expect, it, vi } from "vitest"; + +import { Events } from "../shared/bridge"; +import { KimiRuntime, type OpenSessionOptions } from "../src/runtime/kimi-runtime"; + +const sdkFactories = vi.hoisted(() => { + const harness = { homeDir: "/tmp/kimi-runtime-home", close: vi.fn(async () => undefined) }; + return { + harness, + createKimiHarness: vi.fn(() => harness), + }; +}); + +vi.mock("@moonshot-ai/kimi-code-sdk", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + createKimiHarness: sdkFactories.createKimiHarness, + }; +}); + +interface FakeSessionBoundary { + readonly session: Session; + readonly setModels: string[]; + readonly setThinkingEfforts: ThinkingEffort[]; + readonly setPermissions: PermissionMode[]; + readonly metadataUpdates: JsonObject[]; + readonly handlerInstallations: { approval: number; question: number }; + readonly subscriptionCount: () => number; + readonly closeCount: () => number; + readonly emit: (event: Event) => void; + readonly setPromptImpl: (impl: (input: string | PromptInput) => Promise) => void; +} + +function createFakeSession( + id: string, + workDir: string, + initial: Partial = {}, + metadata?: JsonObject, +): FakeSessionBoundary { + const listeners = new Set<(event: Event) => void>(); + const setModels: string[] = []; + const setThinkingEfforts: ThinkingEffort[] = []; + const setPermissions: PermissionMode[] = []; + const metadataUpdates: JsonObject[] = []; + const handlerInstallations = { approval: 0, question: 0 }; + let subscriptions = 0; + let closes = 0; + let promptImpl: (input: string | PromptInput) => Promise = async () => {}; + let status: SessionStatus = { + model: initial.model ?? "kimi-test", + thinkingEffort: initial.thinkingEffort ?? "off", + permission: initial.permission ?? "manual", + planMode: initial.planMode ?? false, + contextTokens: 0, + maxContextTokens: 128_000, + contextUsage: 0, + }; + let summary: SessionSummary = { + id, + workDir, + sessionDir: `/home/sessions/${id}`, + createdAt: 1, + updatedAt: 2, + metadata, + }; + + const session = { + id, + workDir, + get summary() { + return summary; + }, + set summary(value: SessionSummary | undefined) { + if (value !== undefined) summary = value; + }, + setApprovalHandler(handler: ApprovalHandler | undefined) { + if (handler !== undefined) handlerInstallations.approval += 1; + }, + setQuestionHandler(handler: QuestionHandler | undefined) { + if (handler !== undefined) handlerInstallations.question += 1; + }, + onEvent(listener: (event: Event) => void) { + subscriptions += 1; + listeners.add(listener); + return () => listeners.delete(listener); + }, + async prompt(input: string | PromptInput) { + await promptImpl(input); + }, + async steer(_input: string | PromptInput) {}, + async cancel() {}, + async getStatus() { + return status; + }, + async setModel(model: string) { + setModels.push(model); + status = { ...status, model }; + }, + async setThinking(effort: ThinkingEffort) { + setThinkingEfforts.push(effort); + status = { ...status, thinkingEffort: effort }; + }, + async setPermission(permission: PermissionMode) { + setPermissions.push(permission); + status = { ...status, permission }; + }, + async updateMetadata(patch: JsonObject) { + metadataUpdates.push(patch); + summary = { ...summary, metadata: { ...summary.metadata, ...patch } }; + }, + async close() { + closes += 1; + }, + } as unknown as Session; + + return { + session, + setModels, + setThinkingEfforts, + setPermissions, + metadataUpdates, + handlerInstallations, + subscriptionCount: () => subscriptions, + closeCount: () => closes, + emit: (event: Event) => { + for (const listener of [...listeners]) listener(event); + }, + setPromptImpl: (impl) => { + promptImpl = impl; + }, + }; +} + +interface FakeHarnessBoundary { + readonly harness: KimiHarness; + readonly createInputs: CreateSessionOptions[]; + readonly resumeInputs: ResumeSessionInput[]; + readonly closeSessionIds: string[]; + readonly deleteSessionIds: string[]; + readonly closeCount: () => number; + readonly sessions: Map; + addSession( + id: string, + workDir: string, + initial?: Partial, + metadata?: JsonObject, + ): FakeSessionBoundary; +} + +function createFakeHarness( + normalizeCreatedWorkDir: (workDir: string) => string = (workDir) => workDir, +): FakeHarnessBoundary { + const sessions = new Map(); + const createInputs: CreateSessionOptions[] = []; + const resumeInputs: ResumeSessionInput[] = []; + const closeSessionIds: string[] = []; + const deleteSessionIds: string[] = []; + let creates = 0; + let closes = 0; + + const addSession = ( + id: string, + workDir: string, + initial?: Partial, + metadata?: JsonObject, + ) => { + const boundary = createFakeSession(id, workDir, initial, metadata); + sessions.set(id, boundary); + return boundary; + }; + + const harness = { + async createSession(options: CreateSessionOptions) { + createInputs.push(options); + creates += 1; + return addSession( + `created-${creates}`, + normalizeCreatedWorkDir(options.workDir), + { + model: options.model, + thinkingEffort: options.thinking, + permission: options.permission, + }, + options.metadata, + ).session; + }, + async resumeSession(input: ResumeSessionInput) { + resumeInputs.push(input); + const boundary = sessions.get(input.id); + if (boundary === undefined) throw new Error(`Unknown session: ${input.id}`); + return boundary.session; + }, + async closeSession(id: string) { + closeSessionIds.push(id); + }, + async deleteSession(id: string) { + deleteSessionIds.push(id); + }, + async close() { + closes += 1; + }, + } as unknown as KimiHarness; + + return { + harness, + createInputs, + resumeInputs, + closeSessionIds, + deleteSessionIds, + closeCount: () => closes, + sessions, + addSession, + }; +} + +function openOptions(overrides: Partial = {}): OpenSessionOptions { + return { + webviewId: "view-1", + workDir: "/workspace", + model: "kimi-test", + effort: "off", + yoloMode: false, + ...overrides, + }; +} + +function createRuntime( + normalizeCreatedWorkDir?: (workDir: string) => string, +) { + const sdk = createFakeHarness(normalizeCreatedWorkDir); + const runtime = new KimiRuntime({ + version: "0.6.0", + harness: sdk.harness, + broadcast: () => undefined, + captureBaseline: () => undefined, + log: () => undefined, + }); + return { runtime, sdk }; +} + +describe("Kimi runtime (owns shared SDK sessions for Webviews)", () => { + it("creates the harness through the SDK factory when none is injected", async () => { + const runtime = new KimiRuntime({ + version: "0.6.0", + broadcast: () => undefined, + captureBaseline: () => undefined, + log: () => undefined, + }); + expect(sdkFactories.createKimiHarness).toHaveBeenCalledOnce(); + expect(sdkFactories.createKimiHarness).toHaveBeenCalledWith({ + homeDir: undefined, + identity: { + productName: "kimi-code-vscode", + version: "0.6.0", + platform: "kimi_code_vscode", + }, + uiMode: "vscode", + }); + expect(runtime.harness).toBe(sdkFactories.harness as unknown as KimiHarness); + await runtime.dispose(); + }); + + it("forwards the requested settings when creating an SDK session", async () => { + const { runtime, sdk } = createRuntime(); + + const opened = await runtime.openSession( + openOptions({ model: "kimi-k2", effort: "high", yoloMode: true }), + ); + + expect(sdk.createInputs).toEqual([ + { + workDir: "/workspace", + model: "kimi-k2", + thinking: "high", + permission: "yolo", + metadata: { vscode_legacy_approval: { yolo: true, afk: false } }, + }, + ]); + expect(opened.subscribers).toEqual(["view-1"]); + }); + + it("accepts the normalized SDK workDir when a Windows session is created", async () => { + const { runtime } = createRuntime((workDir) => workDir.replaceAll("\\", "/")); + + const opened = await runtime.openSession(openOptions({ + workDir: "C:\\Users\\Example User\\项目", + })); + + expect(opened.session.workDir).toBe("C:/Users/Example User/项目"); + }); + + it("resumes a Windows session when only separators and casing differ", async () => { + const { runtime, sdk } = createRuntime(); + sdk.addSession("saved-win", "C:/Users/Example User/项目"); + + const opened = await runtime.openSession(openOptions({ + sessionId: "saved-win", + workDir: "c:\\users\\example user\\项目", + })); + + expect(opened.id).toBe("saved-win"); + }); + + it("uses off thinking when a new session receives an empty effort", async () => { + const { runtime, sdk } = createRuntime(); + + await runtime.openSession(openOptions({ effort: " " })); + + expect(sdk.createInputs[0]?.thinking).toBe("off"); + }); + + it("resumes the requested SDK session instead of creating a replacement", async () => { + const { runtime, sdk } = createRuntime(); + sdk.addSession("saved-1", "/workspace"); + + const opened = await runtime.openSession(openOptions({ sessionId: "saved-1" })); + + expect(opened.id).toBe("saved-1"); + expect(sdk.resumeInputs).toEqual([{ id: "saved-1", includeSubagents: true }]); + expect(sdk.createInputs).toEqual([]); + }); + + it("switches a Webview to the requested session", async () => { + const { runtime, sdk } = createRuntime(); + await runtime.openSession(openOptions()); + sdk.addSession("saved-2", "/workspace"); + + const selected = await runtime.openSession(openOptions({ sessionId: "saved-2" })); + + expect(runtime.getSessionForView("view-1")?.id).toBe("saved-2"); + expect(selected.id).toBe("saved-2"); + }); + + it("closes an unshared old session when its Webview switches away", async () => { + const { runtime, sdk } = createRuntime(); + const old = await runtime.openSession(openOptions()); + const oldBoundary = sdk.sessions.get(old.id)!; + sdk.addSession("saved-2", "/workspace"); + + await runtime.openSession(openOptions({ sessionId: "saved-2" })); + + expect(oldBoundary.closeCount()).toBe(1); + }); + + it("shares one SDK subscription when two Webviews attach to the same session", async () => { + const { runtime, sdk } = createRuntime(); + const first = await runtime.openSession(openOptions({ webviewId: "view-1" })); + const boundary = sdk.sessions.get(first.id)!; + + const second = await runtime.openSession( + openOptions({ webviewId: "view-2", sessionId: first.id }), + ); + + expect(second).toBe(first); + expect(first.subscribers).toEqual(["view-1", "view-2"]); + expect(boundary.subscriptionCount()).toBe(1); + expect(boundary.handlerInstallations).toEqual({ approval: 1, question: 1 }); + }); + + it("does not double-wrap the SDK session when two opens race for it", async () => { + const sdk = createFakeHarness(); + const broadcasts: { event: string; data: unknown; webviewId?: string }[] = []; + const runtime = new KimiRuntime({ + version: "0.6.0", + harness: sdk.harness, + broadcast: (event, data, webviewId) => { + broadcasts.push({ event, data, webviewId }); + }, + captureBaseline: () => undefined, + log: () => undefined, + }); + const boundary = sdk.addSession("saved-1", "/workspace"); + + const [first, second] = await Promise.all([ + runtime.openSession(openOptions({ sessionId: "saved-1" })), + runtime.openSession(openOptions({ sessionId: "saved-1" })), + ]); + + expect(second).toBe(first); + expect(boundary.subscriptionCount()).toBe(1); + + boundary.emit({ + type: "assistant.delta", + sessionId: "saved-1", + agentId: "main", + turnId: 1, + delta: "Hello", + }); + + const parts = broadcasts.filter( + ({ data }) => (data as { type?: string }).type === "ContentPart", + ); + expect(parts).toHaveLength(1); + }); + + it("coalesces two concurrent new-session opens for one view onto one session", async () => { + const { runtime, sdk } = createRuntime(); + + const [first, second] = await Promise.all([ + runtime.openSession(openOptions()), + runtime.openSession(openOptions()), + ]); + + expect(second).toBe(first); + expect(sdk.createInputs).toHaveLength(1); + expect(first.subscribers).toEqual(["view-1"]); + }); + + it("does not double-wrap the SDK session when two attaches race for it", async () => { + const sdk = createFakeHarness(); + const broadcasts: { event: string; data: unknown; webviewId?: string }[] = []; + const runtime = new KimiRuntime({ + version: "0.6.0", + harness: sdk.harness, + broadcast: (event, data, webviewId) => { + broadcasts.push({ event, data, webviewId }); + }, + captureBaseline: () => undefined, + log: () => undefined, + }); + const boundary = sdk.addSession("saved-1", "/workspace"); + + const [first, second] = await Promise.all([ + runtime.attachResumedSession("view-1", boundary.session), + runtime.attachResumedSession("view-1", boundary.session), + ]); + + expect(second).toBe(first); + expect(boundary.subscriptionCount()).toBe(1); + + boundary.emit({ + type: "assistant.delta", + sessionId: "saved-1", + agentId: "main", + turnId: 1, + delta: "Hello", + }); + + const parts = broadcasts.filter( + ({ data }) => (data as { type?: string }).type === "ContentPart", + ); + expect(parts).toHaveLength(1); + }); + + it("preserves the resumed session's model instead of reapplying the configured default", async () => { + const { runtime, sdk } = createRuntime(); + const session = sdk.addSession("saved-1", "/workspace", { model: "old-model" }); + + const opened = await runtime.openSession(openOptions({ sessionId: "saved-1", model: "new-model" })); + + expect(session.setModels).toEqual([]); + await expect(opened.session.getStatus()).resolves.toMatchObject({ model: "old-model" }); + }); + + it("preserves the resumed session's thinking effort instead of reapplying the configured default", async () => { + const { runtime, sdk } = createRuntime(); + const session = sdk.addSession("saved-1", "/workspace", { thinkingEffort: "max" }); + + const opened = await runtime.openSession(openOptions({ sessionId: "saved-1", effort: "medium" })); + + expect(session.setThinkingEfforts).toEqual([]); + await expect(opened.session.getStatus()).resolves.toMatchObject({ thinkingEffort: "max" }); + }); + + it("announces the session's actual status to the attaching view so the display matches it", async () => { + const sdk = createFakeHarness(); + const broadcasts: { event: string; data: unknown; webviewId?: string }[] = []; + const runtime = new KimiRuntime({ + version: "0.6.0", + harness: sdk.harness, + broadcast: (event, data, webviewId) => { + broadcasts.push({ event, data, webviewId }); + }, + captureBaseline: () => undefined, + log: () => undefined, + }); + sdk.addSession("saved-1", "/workspace", { + model: "kimi-test", + thinkingEffort: "max", + planMode: true, + }); + + await runtime.openSession(openOptions({ sessionId: "saved-1", effort: "medium" })); + + expect(broadcasts).toContainEqual({ + event: Events.StreamEvent, + data: { + type: "StatusUpdate", + payload: { + model: "kimi-test", + thinking_effort: "max", + plan_mode: true, + context_usage: 0, + }, + _sessionId: "saved-1", + }, + webviewId: "view-1", + }); + }); + + it("uses the yolo setting as the initial value for an unmarked resumed session", async () => { + const { runtime, sdk } = createRuntime(); + const session = sdk.addSession("saved-1", "/workspace", { permission: "manual" }); + + await runtime.openSession(openOptions({ sessionId: "saved-1", yoloMode: true })); + + expect(session.metadataUpdates).toEqual([ + { vscode_legacy_approval: { yolo: true, afk: false } }, + ]); + }); + + it("lets the global yolo setting override a persisted off flag on resume", async () => { + const { runtime, sdk } = createRuntime(); + const session = sdk.addSession( + "saved-1", + "/workspace", + { permission: "manual" }, + { vscode_legacy_approval: { yolo: false, afk: false } }, + ); + + const opened = await runtime.openSession(openOptions({ sessionId: "saved-1", yoloMode: true })); + + expect(session.setPermissions).toEqual(["yolo"]); + expect(session.metadataUpdates).toEqual([ + { vscode_legacy_approval: { yolo: true, afk: false } }, + ]); + expect(opened.legacyApprovalFlags).toEqual({ yolo: true, afk: false }); + }); + + it("lets the global yolo setting disable a persisted session yolo flag on resume", async () => { + const { runtime, sdk } = createRuntime(); + const session = sdk.addSession( + "saved-1", + "/workspace", + { permission: "yolo" }, + { vscode_legacy_approval: { yolo: true, afk: false } }, + ); + + const opened = await runtime.openSession(openOptions({ sessionId: "saved-1", yoloMode: false })); + + expect(session.setPermissions).toEqual(["manual"]); + expect(session.metadataUpdates).toEqual([ + { vscode_legacy_approval: { yolo: false, afk: false } }, + ]); + expect(opened.legacyApprovalFlags).toEqual({ yolo: false, afk: false }); + }); + + it("keeps the persisted afk flag while applying the global yolo setting on resume", async () => { + const { runtime, sdk } = createRuntime(); + const session = sdk.addSession( + "saved-1", + "/workspace", + { permission: "manual" }, + { vscode_legacy_approval: { yolo: false, afk: true } }, + ); + + const opened = await runtime.openSession(openOptions({ sessionId: "saved-1", yoloMode: true })); + + expect(session.setPermissions).toEqual(["auto"]); + expect(opened.legacyApprovalFlags).toEqual({ yolo: true, afk: true }); + }); + + it("restores persisted afk with core auto permission", async () => { + const { runtime, sdk } = createRuntime(); + const session = sdk.addSession( + "saved-1", + "/workspace", + { permission: "manual" }, + { vscode_legacy_approval: { yolo: false, afk: true } }, + ); + + await runtime.openSession(openOptions({ sessionId: "saved-1", yoloMode: false })); + + expect(session.setPermissions).toEqual(["auto"]); + }); + + it("changes the setting-backed yolo flag without clearing session afk", async () => { + const { runtime } = createRuntime(); + const opened = await runtime.openSession(openOptions()); + await opened.toggleLegacyApproval("afk"); + + await runtime.setYoloModeForActiveSessions(true); + + expect(opened.legacyApprovalFlags).toEqual({ yolo: true, afk: true }); + await expect(opened.session.getStatus()).resolves.toMatchObject({ permission: "auto" }); + }); + + it("keeps a shared session open when one of its Webviews detaches", async () => { + const { runtime, sdk } = createRuntime(); + const opened = await runtime.openSession(openOptions({ webviewId: "view-1" })); + await runtime.openSession(openOptions({ webviewId: "view-2", sessionId: opened.id })); + const boundary = sdk.sessions.get(opened.id)!; + + await runtime.detachView("view-1"); + + expect(boundary.closeCount()).toBe(0); + expect(runtime.getSessionForView("view-2")?.id).toBe(opened.id); + }); + + it("closes an SDK session when its last Webview detaches", async () => { + const { runtime, sdk } = createRuntime(); + const opened = await runtime.openSession(openOptions()); + const boundary = sdk.sessions.get(opened.id)!; + + await runtime.detachView("view-1"); + + expect(boundary.closeCount()).toBe(1); + expect(runtime.getSession(opened.id)).toBeUndefined(); + }); + + it("reattaches the same resumed session without replacing its handlers", async () => { + const { runtime, sdk } = createRuntime(); + const boundary = sdk.addSession("saved-1", "/workspace"); + const first = await runtime.attachResumedSession("view-1", boundary.session); + + const second = await runtime.attachResumedSession("view-1", boundary.session); + + expect(second).toBe(first); + expect(boundary.subscriptionCount()).toBe(1); + expect(boundary.handlerInstallations).toEqual({ approval: 1, question: 1 }); + expect(boundary.closeCount()).toBe(0); + }); + + it("removes every Webview mapping when a shared session is closed", async () => { + const { runtime } = createRuntime(); + const opened = await runtime.openSession(openOptions({ webviewId: "view-1" })); + await runtime.openSession(openOptions({ webviewId: "view-2", sessionId: opened.id })); + + await runtime.closeSession(opened.id); + + expect(runtime.getSessionForView("view-1")).toBeUndefined(); + expect(runtime.getSessionForView("view-2")).toBeUndefined(); + }); + + it("delegates deletion after the active session has been closed", async () => { + const { runtime, sdk } = createRuntime(); + const opened = await runtime.openSession(openOptions()); + const boundary = sdk.sessions.get(opened.id)!; + + await runtime.deleteSession(opened.id); + + expect(boundary.closeCount()).toBe(1); + expect(sdk.deleteSessionIds).toEqual([opened.id]); + }); + + it("closes every active SDK session when the host runtime is disposed", async () => { + const { runtime, sdk } = createRuntime(); + const first = await runtime.openSession(openOptions({ webviewId: "view-1" })); + const secondBoundary = sdk.addSession("saved-2", "/workspace"); + await runtime.openSession(openOptions({ webviewId: "view-2", sessionId: "saved-2" })); + + await runtime.dispose(); + + expect(sdk.sessions.get(first.id)?.closeCount()).toBe(1); + expect(secondBoundary.closeCount()).toBe(1); + expect(sdk.closeCount()).toBe(1); + }); + + it("does not retain a resumed session when it belongs to a different working directory", async () => { + const { runtime, sdk } = createRuntime(); + const foreign = sdk.addSession("foreign-1", "/other-workspace"); + + await expect( + runtime.openSession(openOptions({ sessionId: "foreign-1" })), + ).rejects.toThrow("The selected session belongs to a different working directory."); + + expect(runtime.getSession("foreign-1")).toBeUndefined(); + expect(foreign.closeCount()).toBe(1); + }); + + function createRecordingRuntime() { + const sdk = createFakeHarness(); + const broadcasts: Array<{ event: string; data: unknown }> = []; + const runtime = new KimiRuntime({ + version: "test", + harness: sdk.harness, + broadcast: (event, data) => { + broadcasts.push({ event, data }); + }, + captureBaseline: () => undefined, + log: () => undefined, + }); + return { runtime, sdk, broadcasts }; + } + + it("fails a reentrant prompt without disturbing the running turn", async () => { + const { runtime, sdk, broadcasts } = createRecordingRuntime(); + const opened = await runtime.openSession(openOptions()); + const boundary = sdk.sessions.get(opened.id)!; + + let releaseTurn!: () => void; + boundary.setPromptImpl(() => new Promise((resolve) => { + releaseTurn = resolve; + })); + const first = opened.prompt("first message"); + boundary.emit({ type: "turn.started", agentId: "main", sessionId: opened.id, turnId: "t1" } as unknown as Event); + expect(opened.isBusy).toBe(true); + + await expect(opened.prompt("concurrent message")).resolves.toEqual({ status: "failed" }); + + // The rejection surfaces as a mid-turn warning; the active turn is untouched. + expect(opened.isBusy).toBe(true); + const busyWarning = broadcasts.find(({ data }) => (data as { type?: string }).type === "error"); + expect(busyWarning?.data).toMatchObject({ + type: "error", + phase: "runtime", + detail: "A response is already being generated for this session.", + terminal: false, + }); + + boundary.emit({ type: "turn.ended", agentId: "main", sessionId: opened.id, turnId: "t1", reason: "completed" } as unknown as Event); + releaseTurn(); + await expect(first).resolves.toEqual({ status: "finished" }); + expect(opened.isBusy).toBe(false); + }); + + it("rejects a prompt during an exclusive operation with a terminal error", async () => { + const { runtime, broadcasts } = createRecordingRuntime(); + const opened = await runtime.openSession(openOptions()); + + let releaseExclusive!: () => void; + const exclusive = opened.runExclusiveAfterCancelling( + () => new Promise((resolve) => { + releaseExclusive = resolve; + }), + ); + // No active work means no later stream_complete: the rejection must be + // terminal so the caller's composer can unlock. + expect(opened.isBusy).toBe(true); + + await expect(opened.prompt("during fork")).resolves.toEqual({ status: "failed" }); + const rejection = broadcasts.find(({ data }) => (data as { type?: string }).type === "error"); + expect(rejection?.data).toMatchObject({ type: "error", phase: "runtime" }); + expect((rejection?.data as Record)["terminal"]).toBeUndefined(); + + releaseExclusive(); + await exclusive; + expect(opened.isBusy).toBe(false); + }); + + it("marks a mid-turn core error as non-terminal until the turn ends", async () => { + const { runtime, sdk, broadcasts } = createRecordingRuntime(); + const opened = await runtime.openSession(openOptions()); + const boundary = sdk.sessions.get(opened.id)!; + + let releaseTurn!: () => void; + boundary.setPromptImpl(() => new Promise((resolve) => { + releaseTurn = resolve; + })); + const first = opened.prompt("first message"); + boundary.emit({ type: "turn.started", agentId: "main", sessionId: opened.id, turnId: "t1" } as unknown as Event); + + boundary.emit({ + type: "error", + agentId: "main", + sessionId: opened.id, + code: "records.write_failed", + message: "Failed to write agent records: EACCES", + } as unknown as Event); + + // The turn is still running: no settlement, no terminal error on the wire. + expect(opened.isBusy).toBe(true); + const warning = broadcasts.find(({ data }) => (data as { type?: string }).type === "error"); + expect(warning?.data).toMatchObject({ + type: "error", + code: "records.write_failed", + phase: "runtime", + terminal: false, + }); + + boundary.emit({ type: "turn.ended", agentId: "main", sessionId: opened.id, turnId: "t1", reason: "completed" } as unknown as Event); + releaseTurn(); + await expect(first).resolves.toEqual({ status: "finished" }); + expect(opened.isBusy).toBe(false); + }); + + it("keeps preflight failures terminal", async () => { + const { runtime, sdk, broadcasts } = createRecordingRuntime(); + const opened = await runtime.openSession(openOptions()); + const boundary = sdk.sessions.get(opened.id)!; + + boundary.setPromptImpl(() => Promise.reject(new Error("provider down"))); + + await expect(opened.prompt("hi")).resolves.toEqual({ status: "failed" }); + const failure = broadcasts.find(({ data }) => (data as { type?: string }).type === "error"); + expect(failure?.data).toMatchObject({ type: "error", phase: "preflight" }); + expect((failure?.data as Record)["terminal"]).toBeUndefined(); + expect(opened.isBusy).toBe(false); + }); +}); diff --git a/apps/vscode/test/legacy-migration.manager.test.ts b/apps/vscode/test/legacy-migration.manager.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f607dd2f0000ce5ab7208e4769f153d7004ec77 --- /dev/null +++ b/apps/vscode/test/legacy-migration.manager.test.ts @@ -0,0 +1,500 @@ +/** + * Scenario: VS Code discovers and runs legacy kimi-cli migration without touching a real home. + * Responsibilities: source selection, shared-marker suppression, real migration, retry, and clear reports. + * Wiring: real temporary files and the public migration package; no stubbed collaborators. + * Run: pnpm --filter kimi-code test -- legacy-migration.manager.test.ts + */ +import { createHash } from "node:crypto"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { LegacyMigrationManager } from "../src/migration"; + +const temporaryRoots: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryRoots.splice(0).map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +describe("legacy migration manager (discovery and migration coordination)", () => { + it("returns an actionable prompt when the default legacy home contains data", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt).toMatchObject({ + kind: "legacy-migration", + actions: [ + { id: "now", label: "Migrate Now" }, + { id: "later", label: "Later" }, + ], + sources: [ + { + sourceHome: rig.sourceHome, + origin: "default", + hasConfig: true, + }, + ], + }); + }); + + it("suppresses the prompt when the shared marker names the same target", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + await writeSharedMarker(rig.sourceHome, rig.targetHome); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt).toBeNull(); + expect(discovery.suppressedSources).toEqual([ + expect.objectContaining({ sourceHome: rig.sourceHome, hasConfig: true }), + ]); + }); + + it("returns the prompt when the shared marker names a different target", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + await writeSharedMarker(rig.sourceHome, join(rig.root, "another-target")); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt?.sources).toEqual([ + expect.objectContaining({ sourceHome: rig.sourceHome }), + ]); + }); + + it("does not duplicate data when the TUI already migrated the same source", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + const existingSession = join(rig.targetHome, "sessions", "existing", "state.json"); + await mkdir(join(existingSession, ".."), { recursive: true }); + await writeFile(existingSession, '{"custom":{"imported_from_kimi_cli":true}}'); + await writeSharedMarker(rig.sourceHome, rig.targetHome); + + const result = await rig.manager.migrateNow(); + + expect(result.status).toBe("nothing-to-migrate"); + await expect(readFile(existingSession, "utf-8")).resolves.toContain( + "imported_from_kimi_cli", + ); + await expect(readFile(join(rig.targetHome, "config.toml"), "utf-8")).rejects.toThrow(); + }); + + it("conservatively suppresses the prompt when the shared marker is corrupt", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + await writeFile(join(rig.sourceHome, ".migrated-to-kimi-code"), "not-json"); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt).toBeNull(); + expect(discovery.suppressedSources).toHaveLength(1); + }); + + it("discovers a relative legacy KIMI_SHARE_DIR from the workspace and warns about its resolution", async () => { + const rig = await createRig({ + legacyEnvironmentVariables: { KIMI_SHARE_DIR: "legacy-kimi" }, + }); + const shareHome = join(rig.workspaceRoot, "legacy-kimi"); + await writeLegacyConfig(shareHome); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt?.sources).toEqual([ + expect.objectContaining({ + sourceHome: resolve(shareHome), + origin: "legacy-vscode-setting", + }), + ]); + expect(discovery.warnings).toEqual([ + expect.objectContaining({ code: "relative-share-dir", sourceHome: resolve(shareHome) }), + ]); + }); + + it("migrates both the default home and the extra legacy KIMI_SHARE_DIR source", async () => { + const rig = await createRig({ + legacyEnvironmentVariables: { KIMI_SHARE_DIR: "legacy-kimi" }, + }); + await writeLegacyConfig(rig.sourceHome); + const shareHome = join(rig.workspaceRoot, "legacy-kimi"); + await mkdir(join(shareHome, "skills", "example-skill"), { recursive: true }); + await writeFile( + join(shareHome, "skills", "example-skill", "SKILL.md"), + "---\nname: example-skill\ndescription: test fixture\n---\n", + ); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("completed"); + expect(result.sources).toHaveLength(2); + expect(result.totals).toMatchObject({ configFiles: 1, skills: 1 }); + await expect( + readFile(join(rig.targetHome, "skills", "example-skill", "SKILL.md"), "utf-8"), + ).resolves.toContain("example-skill"); + }); + + it("ignores legacy environment variables other than KIMI_SHARE_DIR", async () => { + const rig = await createRig({ + legacyEnvironmentVariables: { + KIMI_CODE_HOME: join(tmpdir(), "must-not-be-read"), + PATH: join(tmpdir(), "must-not-be-used"), + }, + }); + + const discovery = await rig.manager.discover(); + + expect(discovery).toMatchObject({ prompt: null, warnings: [] }); + }); + + it("ignores a non-string legacy KIMI_SHARE_DIR with a clear warning", async () => { + const rig = await createRig({ + legacyEnvironmentVariables: { KIMI_SHARE_DIR: 42, HTTPS_PROXY: "https://example.test" }, + }); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt).toBeNull(); + expect(discovery.warnings).toEqual([ + expect.objectContaining({ + code: "invalid-share-dir", + message: expect.stringContaining("non-empty string"), + }), + ]); + }); + + it("ignores a relative legacy KIMI_SHARE_DIR when no workspace can resolve it", async () => { + const rig = await createRig({ + workspaceRoot: null, + legacyEnvironmentVariables: { KIMI_SHARE_DIR: "legacy-kimi" }, + }); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt).toBeNull(); + expect(discovery.warnings).toEqual([ + expect.objectContaining({ + code: "invalid-share-dir", + message: expect.stringContaining("no workspace is open"), + }), + ]); + }); + + it("bypasses a completed marker on explicit retry and runs the real migration", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + await writeSharedMarker(rig.sourceHome, rig.targetHome); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("completed"); + expect(result.totals.configFiles).toBe(1); + await expect(readFile(join(rig.targetHome, "config.toml"), "utf-8")).resolves.toContain( + "merge_all_available_skills", + ); + }); + + it("keeps the migrated target unchanged when an explicit retry is repeated", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + await rig.manager.retry(); + const firstTarget = await readFile(join(rig.targetHome, "config.toml"), "utf-8"); + + const secondResult = await rig.manager.retry(); + + expect(secondResult.status).toBe("completed"); + await expect(readFile(join(rig.targetHome, "config.toml"), "utf-8")).resolves.toBe( + firstTarget, + ); + }); + + it("reports a corrupt session as a partial migration without rolling back valid data", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + await writeCorruptLegacySession(rig.sourceHome, rig.workspaceRoot); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("partial"); + expect(result.sources[0]?.failures).toEqual([ + expect.objectContaining({ + code: "session-failed", + message: expect.stringMatching(/corrupt|parseable/i), + }), + ]); + await expect(readFile(join(rig.targetHome, "config.toml"), "utf-8")).resolves.toContain( + "merge_all_available_skills", + ); + }); + + it("discovers an unknown workdir bucket as actionable legacy data", async () => { + const rig = await createRig(); + const bucket = await writeUnknownWorkdirBucket(rig.sourceHome); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt?.sources).toEqual([ + expect.objectContaining({ + sourceHome: rig.sourceHome, + totalSessions: 0, + sessionIssues: 1, + }), + ]); + expect(discovery.warnings).toEqual([ + expect.objectContaining({ + code: "legacy-session-unreadable", + message: expect.stringContaining(bucket), + }), + ]); + }); + + it("returns a partial result with a manual action for an unknown workdir bucket", async () => { + const rig = await createRig(); + const bucket = await writeUnknownWorkdirBucket(rig.sourceHome); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("partial"); + expect(result.sources[0]?.failures).toEqual([ + expect.objectContaining({ code: "session-failed", item: bucket }), + ]); + expect(result.manualActions).toEqual([expect.stringContaining(bucket)]); + expect(result.warnings).toEqual([ + expect.objectContaining({ code: "legacy-session-unreadable" }), + ]); + }); + + it("returns a partial result with a warning when a registered bucket is unreadable", async () => { + const rig = await createRig(); + const workDir = rig.workspaceRoot; + await mkdir(rig.sourceHome, { recursive: true }); + await writeFile( + join(rig.sourceHome, "kimi.json"), + JSON.stringify({ work_dirs: [{ path: workDir, kaos: "local" }] }), + ); + const bucket = join( + rig.sourceHome, + "sessions", + createHash("md5").update(workDir).digest("hex"), + ); + await mkdir(join(rig.sourceHome, "sessions"), { recursive: true }); + await writeFile(bucket, "not a directory"); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("partial"); + expect(result.sources[0]?.failures).toEqual([ + expect.objectContaining({ code: "session-failed", item: bucket }), + ]); + expect(result.warnings).toEqual([ + expect.objectContaining({ + code: "legacy-session-unreadable", + message: expect.stringContaining(bucket), + }), + ]); + }); + + it("keeps an unresolved session source discoverable after a partial run", async () => { + const rig = await createRig(); + await writeUnknownWorkdirBucket(rig.sourceHome); + await rig.manager.retry(); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt?.sources).toEqual([ + expect.objectContaining({ sourceHome: rig.sourceHome, sessionIssues: 1 }), + ]); + }); + + it("reports an unresolved session warning when a shared marker suppresses the prompt", async () => { + const rig = await createRig(); + const bucket = await writeUnknownWorkdirBucket(rig.sourceHome); + await writeSharedMarker(rig.sourceHome, rig.targetHome); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt).toBeNull(); + expect(discovery.suppressedSources).toEqual([ + expect.objectContaining({ sourceHome: rig.sourceHome, sessionIssues: 1 }), + ]); + expect(discovery.warnings).toEqual([ + expect.objectContaining({ + code: "legacy-session-unreadable", + message: expect.stringContaining(bucket), + }), + ]); + }); + + it("reports an unparseable legacy config with a manual review action", async () => { + const rig = await createRig(); + await mkdir(rig.sourceHome, { recursive: true }); + await writeFile(join(rig.sourceHome, "config.toml"), 'broken = "unterminated'); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("partial"); + expect(result.sources[0]?.failures).toEqual([ + expect.objectContaining({ code: "legacy-config-unreadable", item: "config.toml" }), + ]); + expect(result.manualActions).toEqual([ + expect.stringContaining("review it manually"), + ]); + }); + + it("reports an unparseable legacy MCP file with a manual review action", async () => { + const rig = await createRig(); + await mkdir(rig.sourceHome, { recursive: true }); + await writeFile(join(rig.sourceHome, "mcp.json"), "{broken"); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("partial"); + expect(result.sources[0]?.failures).toEqual([ + expect.objectContaining({ code: "legacy-mcp-unreadable", item: "mcp.json" }), + ]); + }); + + it("returns a failed result with the original filesystem reason when the target is blocked", async () => { + const rig = await createRig(); + await writeLegacyConfig(rig.sourceHome); + await writeFile(rig.targetHome, "this file blocks the target directory"); + + const result = await rig.manager.retry(); + + expect(result.status).toBe("failed"); + expect(result.sources[0]?.failures).toEqual([ + expect.objectContaining({ + code: "run-failed", + message: expect.stringMatching(/file|directory|exist|not a directory/i), + }), + ]); + expect(result.manualActions).toEqual([ + expect.stringContaining("Migrate Legacy Data"), + ]); + }); + + it("reports legacy OAuth login as requiring a new login without treating it as migratable data", async () => { + const rig = await createRig(); + await mkdir(join(rig.sourceHome, "credentials"), { recursive: true }); + await writeFile(join(rig.sourceHome, "credentials", "kimi-code.json"), "{}"); + + const discovery = await rig.manager.discover(); + + expect(discovery.prompt).toBeNull(); + expect(discovery.notices.oauthLoginsRequiringRelogin).toEqual([ + { sourceHome: rig.sourceHome, name: "kimi-code" }, + ]); + }); + + it("reports legacy MCP OAuth state as requiring reauthorization", async () => { + const rig = await createRig(); + await mkdir(rig.sourceHome, { recursive: true }); + await writeFile( + join(rig.sourceHome, "mcp.json"), + JSON.stringify({ + mcpServers: { + "example-server": { url: "https://example.test/mcp", auth: "oauth" }, + plain: { command: "npx" }, + }, + }), + ); + await mkdir(join(rig.sourceHome, "mcp-oauth"), { recursive: true }); + await writeFile(join(rig.sourceHome, "mcp-oauth", "mangled-store-entry"), "{}"); + + const discovery = await rig.manager.discover(); + + expect(discovery.notices.mcpOauthServersRequiringReauth).toEqual([ + { sourceHome: rig.sourceHome, name: "example-server" }, + ]); + }); + + it("reports a configured migration source that is not a directory", async () => { + const rig = await createRig(); + await writeFile(rig.sourceHome, "not a directory"); + + const discovery = await rig.manager.discover(); + + expect(discovery.warnings).toEqual([ + expect.objectContaining({ + code: "source-not-directory", + sourceHome: rig.sourceHome, + }), + ]); + }); +}); + +interface RigOptions { + readonly workspaceRoot?: string | null; + readonly legacyEnvironmentVariables?: unknown; +} + +async function createRig(options: RigOptions = {}): Promise<{ + readonly root: string; + readonly sourceHome: string; + readonly targetHome: string; + readonly workspaceRoot: string; + readonly manager: LegacyMigrationManager; +}> { + const root = await mkdtemp(join(tmpdir(), "vscode-legacy-migration-")); + temporaryRoots.push(root); + const sourceHome = join(root, ".kimi"); + const targetHome = join(root, ".kimi-code"); + const workspaceRoot = join(root, "workspace"); + await mkdir(options.workspaceRoot === null ? root : workspaceRoot, { recursive: true }); + const manager = new LegacyMigrationManager({ + targetHome, + defaultSourceHome: sourceHome, + workspaceRoot: options.workspaceRoot === undefined ? workspaceRoot : options.workspaceRoot, + legacyEnvironmentVariables: options.legacyEnvironmentVariables, + plansSourceDir: join(root, "plans"), + }); + return { root, sourceHome, targetHome, workspaceRoot, manager }; +} + +async function writeLegacyConfig(sourceHome: string): Promise { + await mkdir(sourceHome, { recursive: true }); + await writeFile(join(sourceHome, "config.toml"), "merge_all_available_skills = true\n"); +} + +async function writeSharedMarker(sourceHome: string, targetHome: string): Promise { + await mkdir(sourceHome, { recursive: true }); + await writeFile( + join(sourceHome, ".migrated-to-kimi-code"), + JSON.stringify({ version: 1, target_path: targetHome, runs: [] }), + ); +} + +async function writeCorruptLegacySession( + sourceHome: string, + workDir: string, +): Promise { + await mkdir(sourceHome, { recursive: true }); + await writeFile( + join(sourceHome, "kimi.json"), + JSON.stringify({ work_dirs: [{ path: workDir, kaos: "local" }] }), + ); + const bucket = createHash("md5").update(workDir).digest("hex"); + const sessionDir = join(sourceHome, "sessions", bucket, "corrupt-session"); + await mkdir(sessionDir, { recursive: true }); + await writeFile(join(sessionDir, "context.jsonl"), "not-json\n{broken\n}}}\n"); + await writeFile(join(sessionDir, "state.json"), "{}"); +} + +async function writeUnknownWorkdirBucket(sourceHome: string): Promise { + const bucket = join( + sourceHome, + "sessions", + createHash("md5").update("/workspace/not-registered").digest("hex"), + ); + await mkdir(join(bucket, "legacy-session"), { recursive: true }); + await writeFile( + join(bucket, "legacy-session", "context.jsonl"), + '{"role":"user","content":"hello"}\n', + ); + return bucket; +} diff --git a/apps/vscode/test/replay-adapter.test.ts b/apps/vscode/test/replay-adapter.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..34ea632b823b9f23d1afbb6dc63ad7eeb8331529 --- /dev/null +++ b/apps/vscode/test/replay-adapter.test.ts @@ -0,0 +1,569 @@ +/** + * Scenario: a resumed Node SDK transcript is rendered through the released VS Code Webview protocol. + * Responsibilities: visible turns, media, assistant/tool output, subagent routing, compaction, plan state, and hidden injections. + * Wiring: the pure replay adapter and public SDK replay types are used directly; there are no stubs. + * Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts test/replay-adapter.test.ts + */ + +import type { + AgentReplayRecord, + ContentPart, + ResumedAgentState, + ResumedSessionState, + ToolCall, +} from "@moonshot-ai/kimi-code-sdk"; +import { describe, expect, it } from "vitest"; + +import { + replayRecordTurnCount, + replaySessionToWebviewEvents, + replayToWebviewEvents, +} from "../src/runtime/replay-adapter"; + +type ReplayMessage = Extract["message"]; + +function message( + role: ReplayMessage["role"], + content: ContentPart[], + options: { + readonly toolCalls?: ToolCall[]; + readonly toolCallId?: string; + readonly isError?: boolean; + readonly origin?: ReplayMessage["origin"]; + } = {}, +): ReplayMessage { + return { + role, + content, + toolCalls: options.toolCalls ?? [], + toolCallId: options.toolCallId, + isError: options.isError, + origin: options.origin, + }; +} + +function record(messageValue: ReplayMessage, time: number = 1): AgentReplayRecord { + return { type: "message", message: messageValue, time }; +} + +function resumedAgent( + replay: readonly AgentReplayRecord[], + options: { + readonly modelAlias?: string; + readonly thinkingEffort?: string; + readonly plan?: ResumedAgentState["plan"]; + readonly contextTokenCount?: number; + readonly usage?: ResumedAgentState["usage"]; + readonly type?: ResumedAgentState["type"]; + } = {}, +): ResumedAgentState { + return { + type: options.type ?? "main", + config: { + cwd: "/workspace", + modelAlias: options.modelAlias ?? "kimi-test", + modelCapabilities: { + image_in: true, + video_in: true, + audio_in: false, + thinking: true, + tool_use: true, + max_context_tokens: 128_000, + }, + thinkingEffort: options.thinkingEffort ?? "off", + systemPrompt: "", + }, + context: { history: [], tokenCount: options.contextTokenCount ?? 0 }, + replay, + permission: { mode: "manual", rules: [] }, + plan: options.plan ?? null, + usage: options.usage ?? {}, + tools: [], + background: [], + }; +} + +function replay(records: readonly AgentReplayRecord[]) { + return replayToWebviewEvents(resumedAgent(records), "session-1"); +} + +describe("replay adapter (renders the public SDK resume state for the Webview)", () => { + it("restores the selected model before transcript events", () => { + const agent = resumedAgent([]); + + expect(replayToWebviewEvents(agent, "session-1")[0]).toMatchObject({ + type: "StatusUpdate", + payload: { model: "kimi-test" }, + }); + }); + + it("restores thinking effort before transcript events", () => { + const agent = resumedAgent([], { thinkingEffort: "high" }); + + expect(replayToWebviewEvents(agent, "session-1")[0]).toMatchObject({ + type: "StatusUpdate", + payload: { thinking_effort: "high" }, + }); + }); + + it("restores active plan mode before transcript events", () => { + const agent = resumedAgent([], { + plan: { id: "plan-1", content: "Plan", path: "/workspace/plan.md" }, + }); + + expect(replayToWebviewEvents(agent, "session-1")[0]).toMatchObject({ + type: "StatusUpdate", + payload: { plan_mode: true }, + }); + }); + + it("restores context usage after all transcript turns", () => { + const agent = resumedAgent([], { contextTokenCount: 32_000 }); + + expect(replayToWebviewEvents(agent, "session-1").at(-1)).toMatchObject({ + type: "StatusUpdate", + payload: { context_usage: 0.25 }, + }); + }); + + it("restores cumulative token usage exactly once after all transcript turns", () => { + const agent = resumedAgent([], { + usage: { + total: { + inputOther: 10, + output: 4, + inputCacheRead: 3, + inputCacheCreation: 2, + }, + }, + }); + + const statusEvents = replayToWebviewEvents(agent, "session-1").filter( + (event) => + event.type === "StatusUpdate" && + typeof event.payload === "object" && + event.payload !== null && + "token_usage" in event.payload, + ); + expect(statusEvents).toEqual([ + { + type: "StatusUpdate", + payload: { + context_usage: 0, + token_usage: { + input_other: 10, + output: 4, + input_cache_read: 3, + input_cache_creation: 2, + }, + }, + _sessionId: "session-1", + }, + ]); + }); + + it("opens one visible turn when replay contains a user prompt", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Fix the test" }], { origin: { kind: "user" } })), + ]); + + expect(events.filter((event) => event.type !== "StatusUpdate")).toEqual([ + { + type: "TurnBegin", + payload: { user_input: [{ type: "text", text: "Fix the test" }] }, + _sessionId: "session-1", + }, + { + type: "stream_complete", + result: { status: "finished" }, + _sessionId: "session-1", + }, + ]); + }); + + it("converts SDK media keys when replay renders a user prompt", () => { + const events = replay([ + record( + message( + "user", + [ + { type: "image_url", imageUrl: { url: "file:///workspace/a.png", id: "image-1" } }, + { type: "audio_url", audioUrl: { url: "file:///workspace/a.mp3", id: "audio-1" } }, + { type: "video_url", videoUrl: { url: "file:///workspace/a.mp4", id: "video-1" } }, + ], + { origin: { kind: "user" } }, + ), + ), + ]); + + expect(events.find((event) => event.type === "TurnBegin")).toEqual({ + type: "TurnBegin", + payload: { + user_input: [ + { type: "image_url", image_url: { url: "file:///workspace/a.png", id: "image-1" } }, + { type: "audio_url", audio_url: { url: "file:///workspace/a.mp3", id: "audio-1" } }, + { type: "video_url", video_url: { url: "file:///workspace/a.mp4", id: "video-1" } }, + ], + }, + _sessionId: "session-1", + }); + }); + + it("renders assistant text inside the open turn", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Explain" }], { origin: { kind: "user" } })), + record(message("assistant", [{ type: "text", text: "Here is the answer" }]), 2), + ]); + + expect(events.filter((event) => event.type === "ContentPart")).toEqual([ + { + type: "ContentPart", + payload: { type: "text", text: "Here is the answer" }, + _sessionId: "session-1", + }, + ]); + }); + + it("renders signed thinking inside the open turn", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Explain" }], { origin: { kind: "user" } })), + record( + message("assistant", [{ type: "think", think: "Reviewing", encrypted: "signature" }]), + 2, + ), + ]); + + expect(events.filter((event) => event.type === "ContentPart")).toEqual([ + { + type: "ContentPart", + payload: { type: "think", think: "Reviewing", encrypted: "signature" }, + _sessionId: "session-1", + }, + ]); + }); + + it("renders assistant media when a resumed answer contains media parts", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Show it" }], { origin: { kind: "user" } })), + record( + message("assistant", [ + { type: "image_url", imageUrl: { url: "https://example.test/result.png" } }, + { type: "video_url", videoUrl: { url: "https://example.test/result.mp4" } }, + ]), + 2, + ), + ]); + + expect(events.filter((event) => event.type === "ContentPart")).toEqual([ + { + type: "ContentPart", + payload: { type: "image_url", image_url: { url: "https://example.test/result.png" } }, + _sessionId: "session-1", + }, + { + type: "ContentPart", + payload: { type: "video_url", video_url: { url: "https://example.test/result.mp4" } }, + _sessionId: "session-1", + }, + ]); + }); + + it("maps an assistant tool call to the released tool name", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Read it" }], { origin: { kind: "user" } })), + record( + message("assistant", [], { + toolCalls: [ + { + type: "function", + id: "tool-1", + name: "Read", + arguments: '{"path":"README.md"}', + }, + ], + }), + 2, + ), + ]); + + expect(events).toContainEqual({ + type: "ToolCall", + payload: { + type: "function", + id: "tool-1", + function: { name: "ReadFile", arguments: '{"path":"README.md"}' }, + }, + _sessionId: "session-1", + }); + }); + + it("renders a failed tool result in the open turn", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Run it" }], { origin: { kind: "user" } })), + record( + message("tool", [{ type: "text", text: "command failed" }], { + toolCallId: "tool-1", + isError: true, + }), + 2, + ), + ]); + + expect(events).toContainEqual({ + type: "ToolResult", + payload: { + tool_call_id: "tool-1", + return_value: { + is_error: true, + output: [{ type: "text", text: "command failed" }], + message: "", + display: [], + }, + }, + _sessionId: "session-1", + }); + }); + + it("closes a completed compaction when its replay record has a result", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Continue" }], { origin: { kind: "user" } })), + { + type: "compaction", + time: 2, + result: { + summary: "Earlier work", + compactedCount: 6, + tokensBefore: 1000, + tokensAfter: 200, + }, + }, + ]); + + expect(events.filter((event) => event.type.startsWith("Compaction"))).toEqual([ + { type: "CompactionBegin", payload: {}, _sessionId: "session-1" }, + { type: "CompactionEnd", payload: {}, _sessionId: "session-1" }, + ]); + }); + + it("leaves an unfinished compaction open when its replay record has no result", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Continue" }], { origin: { kind: "user" } })), + { type: "compaction", time: 2, instruction: "Keep decisions" }, + ]); + + expect(events.filter((event) => event.type.startsWith("Compaction"))).toEqual([ + { type: "CompactionBegin", payload: {}, _sessionId: "session-1" }, + ]); + }); + + it("renders plan mode when replay records a plan update inside a turn", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "Make a plan" }], { origin: { kind: "user" } })), + { type: "plan_updated", time: 2, enabled: true }, + ]); + + expect(events).toContainEqual({ + type: "StatusUpdate", + payload: { plan_mode: true }, + _sessionId: "session-1", + }); + }); + + it("separates consecutive user prompts into independently completed turns", () => { + const events = replay([ + record(message("user", [{ type: "text", text: "First" }], { origin: { kind: "user" } }), 1), + record(message("assistant", [{ type: "text", text: "One" }]), 2), + record(message("user", [{ type: "text", text: "Second" }], { origin: { kind: "user" } }), 3), + record(message("assistant", [{ type: "text", text: "Two" }]), 4), + ]); + + expect(events.filter((event) => event.type !== "StatusUpdate").map((event) => event.type)).toEqual([ + "TurnBegin", + "StepBegin", + "ContentPart", + "stream_complete", + "TurnBegin", + "StepBegin", + "ContentPart", + "stream_complete", + ]); + }); + + it("does not expose an injected user message in resumed history", () => { + const events = replay([ + record( + message("user", [{ type: "text", text: "hidden reminder" }], { + origin: { kind: "injection", variant: "system_reminder" }, + }), + ), + record(message("user", [{ type: "text", text: "Visible prompt" }], { origin: { kind: "user" } }), 2), + ]); + + expect(events.filter((event) => event.type === "TurnBegin")).toEqual([ + { + type: "TurnBegin", + payload: { user_input: [{ type: "text", text: "Visible prompt" }] }, + _sessionId: "session-1", + }, + ]); + }); + + it("restores a user-invoked skill as its original slash command", () => { + const events = replay([ + record( + message("user", [{ type: "text", text: "" }], { + origin: { + kind: "skill_activation", + activationId: "activation-1", + skillName: "review", + skillArgs: "focus on errors", + trigger: "user-slash", + }, + }), + ), + ]); + + expect(events).toContainEqual({ + type: "TurnBegin", + payload: { user_input: [{ type: "text", text: "/skill:review focus on errors" }] }, + _sessionId: "session-1", + }); + }); + + it("restores imported context as the original command and confirmation", () => { + const events = replay([ + record( + message( + "user", + [ + { + type: "text", + text: "The user imported prior context.", + }, + { + type: "text", + text: '\nPrior decision.\n', + }, + ], + { origin: { kind: "user" } }, + ), + ), + ]); + + expect(events).toContainEqual({ + type: "TurnBegin", + payload: { user_input: [{ type: "text", text: "/import notes.md" }] }, + _sessionId: "session-1", + }); + expect(events).toContainEqual({ + type: "ContentPart", + payload: { type: "text", text: "Imported context from file 'notes.md' (15 chars)." }, + _sessionId: "session-1", + }); + expect(JSON.stringify(events)).not.toContain("Prior decision."); + }); + + it("does not count a model-invoked skill as a user-visible fork turn", () => { + const records: AgentReplayRecord[] = [ + record( + message("user", [{ type: "text", text: "" }], { + origin: { + kind: "skill_activation", + activationId: "activation-model", + skillName: "helper", + trigger: "model-tool", + }, + }), + ), + ]; + + expect(replayRecordTurnCount(records)).toBe(0); + expect(replay(records).filter((event) => event.type === "TurnBegin")).toEqual([]); + }); + + it("counts only visible prompts when replay reports the number of turns", () => { + const records: AgentReplayRecord[] = [ + record(message("user", [{ type: "text", text: "First" }], { origin: { kind: "user" } }), 1), + record( + message("user", [{ type: "text", text: "Hidden" }], { + origin: { kind: "injection", variant: "system_reminder" }, + }), + 2, + ), + record( + message("user", [{ type: "text", text: "/review" }], { + origin: { + kind: "plugin_command", + activationId: "activation-1", + pluginId: "review-plugin", + commandName: "review", + trigger: "user-slash", + }, + }), + 3, + ), + ]; + + expect(replayRecordTurnCount(records)).toBe(2); + }); + + it("routes repeated runs of one subagent to their corresponding Agent calls", () => { + const main = resumedAgent([ + record(message("user", [{ type: "text", text: "First" }], { origin: { kind: "user" } }), 1), + record(message("assistant", [], { + toolCalls: [{ type: "function", id: "agent-call-1", name: "Agent", arguments: "{}" }], + }), 2), + record(message("tool", [{ type: "text", text: "agent_id: sub-1\nstatus: completed" }], { + toolCallId: "agent-call-1", + }), 5), + record(message("user", [{ type: "text", text: "Second" }], { origin: { kind: "user" } }), 10), + record(message("assistant", [], { + toolCalls: [{ type: "function", id: "agent-call-2", name: "Agent", arguments: "{}" }], + }), 11), + record(message("tool", [{ type: "text", text: "agent_id: sub-1\nstatus: completed" }], { + toolCallId: "agent-call-2", + }), 14), + ]); + const child = resumedAgent([ + record(message("user", [{ type: "text", text: "child one" }], { + origin: { kind: "system_trigger", name: "subagent" }, + }), 3), + record(message("assistant", [{ type: "text", text: "first child answer" }]), 4), + record(message("user", [{ type: "text", text: "child two" }], { + origin: { kind: "system_trigger", name: "subagent" }, + }), 12), + record(message("assistant", [{ type: "text", text: "second child answer" }]), 13), + ], { type: "sub" }); + const state: ResumedSessionState = { + sessionMetadata: { + createdAt: "", + updatedAt: "", + title: "", + isCustomTitle: false, + agents: { + main: { type: "main", parentAgentId: null }, + "sub-1": { type: "sub", parentAgentId: "main" }, + }, + custom: {}, + }, + agents: { main, "sub-1": child }, + }; + + const events = replaySessionToWebviewEvents(state, "session-1"); + + expect(events).toContainEqual(expect.objectContaining({ + type: "SubagentEvent", + payload: { + parent_tool_call_id: "agent-call-1", + event: { type: "ContentPart", payload: { type: "text", text: "first child answer" } }, + }, + })); + expect(events).toContainEqual(expect.objectContaining({ + type: "SubagentEvent", + payload: { + parent_tool_call_id: "agent-call-2", + event: { type: "ContentPart", payload: { type: "text", text: "second child answer" } }, + }, + })); + }); +}); diff --git a/apps/vscode/test/replay-resume.integration.test.ts b/apps/vscode/test/replay-resume.integration.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..0973ab2106f2374f82bef9dee65a0547d56883e1 --- /dev/null +++ b/apps/vscode/test/replay-resume.integration.test.ts @@ -0,0 +1,335 @@ +/** + * Scenario: persisted Node SDK sessions are reopened and rendered by the VS Code replay adapter. + * Responsibilities: restored tool displays and child-agent steps through the public resume state. + * Wiring: Node SDK, core, storage, and HTTP provider adapter are real; only the remote provider is local. + * Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/replay-resume.integration.test.ts + */ + +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; + +import { + createKimiHarness, + type Event, + type KimiHarness, + type Session, +} from "@moonshot-ai/kimi-code-sdk"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + createFakeProviderHarness, + type FakeProviderHarness, +} from "../../../packages/kosong/test/e2e/fake-provider-harness"; +import { replaySessionToWebviewEvents } from "../src/runtime/replay-adapter"; + +const MODEL_ALIAS = "vscode-replay-test"; + +interface ReplayRig { + readonly rootDir: string; + readonly workDir: string; + readonly harness: KimiHarness; + readonly provider: FakeProviderHarness; +} + +const cleanups: Array<() => Promise> = []; + +afterEach(async () => { + while (cleanups.length > 0) await cleanups.pop()?.(); +}); + +async function createReplayRig(): Promise { + const rootDir = await mkdtemp(join(tmpdir(), "kimi-vscode-replay-")); + const homeDir = join(rootDir, "home"); + const workDir = join(rootDir, "workspace"); + await Promise.all([mkdir(homeDir), mkdir(workDir)]); + const provider = await createFakeProviderHarness(); + const harness = createKimiHarness({ + homeDir, + identity: { productName: "kimi-code-vscode", version: "test", platform: "kimi_code_vscode" }, + }); + await harness.setConfig({ + providers: { + local: { + type: "kimi", + baseUrl: `${provider.baseUrl}/v1`, + apiKey: "sk-test", + }, + }, + models: { + [MODEL_ALIAS]: { + provider: "local", + model: "mock-model", + maxContextSize: 128_000, + }, + }, + defaultModel: MODEL_ALIAS, + }); + cleanups.push(async () => { + try { + await harness.close(); + } finally { + try { + await provider.close(); + } finally { + await removeTempDir(rootDir); + } + } + }); + return { rootDir, workDir, harness, provider }; +} + +async function removeTempDir(dir: string): Promise { + for (let attempt = 0; attempt < 10; attempt += 1) { + try { + await rm(dir, { recursive: true, force: true }); + return; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "ENOTEMPTY" && code !== "EBUSY" && code !== "EPERM") { + throw error; + } + await delay(10); + } + } + + await rm(dir, { recursive: true, force: true }); +} + +function completionChunk( + delta: Record, + finishReason: string | null = null, +): Record { + return { + id: "chatcmpl-vscode-replay", + object: "chat.completion.chunk", + created: 1, + model: "mock-model", + choices: [{ index: 0, delta, finish_reason: finishReason }], + }; +} + +async function runPrompt(session: Session, prompt: string): Promise { + const ended = waitForEvent( + session, + (event) => event.type === "turn.ended" && event.agentId === "main", + ); + await session.prompt(prompt); + await ended; +} + +function waitForEvent( + session: Session, + predicate: (event: Event) => boolean, +): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + unsubscribe(); + reject(new Error("Timed out waiting for session event")); + }, 5_000); + const unsubscribe = session.onEvent((event) => { + if (!predicate(event)) return; + clearTimeout(timeout); + unsubscribe(); + resolve(event); + }); + }); +} + +describe("VS Code replay from a public Node SDK resume state", () => { + it("restores persisted file and todo displays", async () => { + const rig = await createReplayRig(); + const filePath = join(rig.workDir, "sample.txt"); + await writeFile(filePath, "before\n", "utf8"); + let requestCount = 0; + rig.provider.route("POST", "/v1/chat/completions", async (_request, reply) => { + requestCount += 1; + if (requestCount === 1) { + await reply.sseJson(200, [ + completionChunk({ + tool_calls: [ + { + index: 0, + id: "edit-call-1", + type: "function", + function: { + name: "Edit", + arguments: JSON.stringify({ + path: "sample.txt", + old_string: "before", + new_string: "after", + }), + }, + }, + { + index: 1, + id: "write-call-1", + type: "function", + function: { + name: "Write", + arguments: JSON.stringify({ + path: "created.txt", + content: "created content\n", + }), + }, + }, + { + index: 2, + id: "todo-call-1", + type: "function", + function: { + name: "TodoList", + arguments: JSON.stringify({ + todos: [{ title: "Verify resume", status: "done" }], + }), + }, + }, + ], + }), + completionChunk({}, "tool_calls"), + ]); + return; + } + await reply.sseJson(200, [ + completionChunk({ content: "Changes complete." }), + completionChunk({}, "stop"), + ]); + }); + const session = await rig.harness.createSession({ + id: "ses_vscode_replay_displays", + workDir: rig.workDir, + model: MODEL_ALIAS, + }); + await session.setPermission("yolo"); + await runPrompt(session, "Update the file and checklist"); + await session.close(); + + const resumed = await rig.harness.resumeSession({ + id: session.id, + includeSubagents: true, + }); + const state = resumed.getResumeState(); + if (state === undefined) throw new Error("Expected public resume state"); + const events = replaySessionToWebviewEvents(state, resumed.id); + + expect(events).toContainEqual( + expect.objectContaining({ + type: "ToolResult", + payload: expect.objectContaining({ + tool_call_id: "write-call-1", + return_value: expect.objectContaining({ + display: [{ + type: "diff", + path: join(rig.workDir, "created.txt"), + old_text: "", + new_text: "created content\n", + }], + }), + }), + }), + ); + expect(events).toContainEqual( + expect.objectContaining({ + type: "ToolResult", + payload: expect.objectContaining({ + tool_call_id: "edit-call-1", + return_value: expect.objectContaining({ + display: [{ type: "diff", path: filePath, old_text: "before", new_text: "after" }], + }), + }), + }), + ); + expect(events).toContainEqual( + expect.objectContaining({ + type: "ToolResult", + payload: expect.objectContaining({ + tool_call_id: "todo-call-1", + return_value: expect.objectContaining({ + display: [{ + type: "todo", + items: [{ title: "Verify resume", status: "done" }], + }], + }), + }), + }), + ); + }); + + it("restores a child step under its original Agent tool call", async () => { + const rig = await createReplayRig(); + const childAnswer = `Subagent restored evidence. ${"Detailed persisted finding. ".repeat(10)}`; + let requestCount = 0; + rig.provider.route("POST", "/v1/chat/completions", async (_request, reply) => { + requestCount += 1; + if (requestCount === 1) { + await reply.sseJson(200, [ + completionChunk({ + tool_calls: [{ + index: 0, + id: "agent-call-1", + type: "function", + function: { + name: "Agent", + arguments: JSON.stringify({ + prompt: "Inspect the workspace and report one finding.", + description: "inspect workspace", + subagent_type: "coder", + run_in_background: false, + }), + }, + }], + }), + completionChunk({}, "tool_calls"), + ]); + return; + } + if (requestCount === 2) { + await reply.sseJson(200, [ + completionChunk({ content: childAnswer }), + completionChunk({}, "stop"), + ]); + return; + } + await reply.sseJson(200, [ + completionChunk({ content: "Parent received the finding." }), + completionChunk({}, "stop"), + ]); + }); + const session = await rig.harness.createSession({ + id: "ses_vscode_replay_subagent", + workDir: rig.workDir, + model: MODEL_ALIAS, + }); + await session.setPermission("yolo"); + await runPrompt(session, "Delegate this inspection"); + await session.close(); + + const resumed = await rig.harness.resumeSession({ + id: session.id, + includeSubagents: true, + }); + const state = resumed.getResumeState(); + if (state === undefined) throw new Error("Expected public resume state"); + const events = replaySessionToWebviewEvents(state, resumed.id); + + expect(events).toContainEqual( + expect.objectContaining({ + type: "SubagentEvent", + payload: { + parent_tool_call_id: "agent-call-1", + event: { type: "StepBegin", payload: { n: 1 } }, + }, + }), + ); + expect(events).toContainEqual( + expect.objectContaining({ + type: "SubagentEvent", + payload: { + parent_tool_call_id: "agent-call-1", + event: { type: "ContentPart", payload: { type: "text", text: childAnswer } }, + }, + }), + ); + }); +}); diff --git a/apps/vscode/test/session-runtime.test.ts b/apps/vscode/test/session-runtime.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..21a63ff147bb2f2f5915f5547b7ba0cef94196b6 --- /dev/null +++ b/apps/vscode/test/session-runtime.test.ts @@ -0,0 +1,763 @@ +/** + * Scenario: one VS Code session runtime adapts the public Node SDK session for one or more Webviews. + * Responsibilities: prompt conversion, event/terminal delivery, reverse RPC, cancellation, and baseline capture. + * Wiring: SessionRuntime is real; a small in-memory Session implements only the public SDK boundary. + * Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts apps/vscode/test/session-runtime.test.ts + */ + +import type { + ApprovalHandler, + ApprovalRequest, + Event, + JsonObject, + PermissionMode, + PromptInput, + QuestionHandler, + QuestionRequest, + Session, + SessionSummary, +} from "@moonshot-ai/kimi-code-sdk"; +import { describe, expect, it } from "vitest"; + +import { Events } from "../shared/bridge"; +import type { LegacyApprovalFlags } from "../src/runtime/legacy-approval"; +import { SessionRuntime } from "../src/runtime/session-runtime"; + +interface BroadcastRecord { + readonly event: string; + readonly data: unknown; + readonly webviewId?: string; +} + +interface BaselineRecord { + readonly session: Pick; + readonly filePath: string; + readonly webviewIds: readonly string[]; +} + +interface FakeSessionBoundary { + readonly session: Session; + readonly promptInputs: Array; + readonly steerInputs: Array; + readonly handlerInstallations: { approval: number; question: number }; + readonly metadataUpdates: JsonObject[]; + readonly setPermissions: PermissionMode[]; + readonly subscriptionCount: () => number; + readonly cancelCount: () => number; + readonly cancelCompactionCount: () => number; + readonly closeCount: () => number; + emit(event: Event): void; + rejectNextPrompt(error: Error): void; + rejectNextMetadataUpdate(error: Error): void; + requestApproval(request: ApprovalRequest): Promise>>; + requestQuestion(request: QuestionRequest): Promise>>; +} + +const DEFAULT_LEGACY_APPROVAL: LegacyApprovalFlags = { yolo: false, afk: false }; + +function createFakeSession(): FakeSessionBoundary { + const listeners = new Set<(event: Event) => void>(); + const promptInputs: Array = []; + const steerInputs: Array = []; + const handlerInstallations = { approval: 0, question: 0 }; + const metadataUpdates: JsonObject[] = []; + const setPermissions: PermissionMode[] = []; + let approvalHandler: ApprovalHandler | undefined; + let questionHandler: QuestionHandler | undefined; + let nextPromptError: Error | undefined; + let nextMetadataError: Error | undefined; + let subscriptions = 0; + let cancellations = 0; + let compactionCancellations = 0; + let closes = 0; + let permission: PermissionMode = "manual"; + + const summary: SessionSummary = { + id: "session-1", + workDir: "/workspace", + sessionDir: "/home/sessions/session-1", + createdAt: 1, + updatedAt: 2, + metadata: { source: "vscode-test" }, + }; + + const session = { + id: summary.id, + workDir: summary.workDir, + summary, + setApprovalHandler(handler: ApprovalHandler | undefined) { + approvalHandler = handler; + if (handler !== undefined) handlerInstallations.approval += 1; + }, + setQuestionHandler(handler: QuestionHandler | undefined) { + questionHandler = handler; + if (handler !== undefined) handlerInstallations.question += 1; + }, + onEvent(listener: (event: Event) => void) { + subscriptions += 1; + listeners.add(listener); + return () => listeners.delete(listener); + }, + async prompt(input: string | PromptInput) { + promptInputs.push(input); + if (nextPromptError !== undefined) { + const error = nextPromptError; + nextPromptError = undefined; + throw error; + } + }, + async steer(input: string | PromptInput) { + steerInputs.push(input); + }, + async cancel() { + cancellations += 1; + }, + async cancelCompaction() { + compactionCancellations += 1; + }, + async getStatus() { + return { + thinkingEffort: "off", + permission, + planMode: false, + contextTokens: 0, + maxContextTokens: 128_000, + contextUsage: 0, + }; + }, + async setPermission(mode: PermissionMode) { + permission = mode; + setPermissions.push(mode); + }, + async updateMetadata(patch: JsonObject) { + if (nextMetadataError !== undefined) { + const error = nextMetadataError; + nextMetadataError = undefined; + throw error; + } + metadataUpdates.push(patch); + }, + async close() { + closes += 1; + }, + } as unknown as Session; + + return { + session, + promptInputs, + steerInputs, + handlerInstallations, + metadataUpdates, + setPermissions, + subscriptionCount: () => subscriptions, + cancelCount: () => cancellations, + cancelCompactionCount: () => compactionCancellations, + closeCount: () => closes, + emit(event) { + for (const listener of listeners) listener(event); + }, + rejectNextPrompt(error) { + nextPromptError = error; + }, + rejectNextMetadataUpdate(error) { + nextMetadataError = error; + }, + async requestApproval(request) { + if (approvalHandler === undefined) throw new Error("Approval handler is unavailable"); + return approvalHandler(request); + }, + async requestQuestion(request) { + if (questionHandler === undefined) throw new Error("Question handler is unavailable"); + return questionHandler(request); + }, + }; +} + +function createRuntime(legacyApproval = DEFAULT_LEGACY_APPROVAL) { + const sdk = createFakeSession(); + const broadcasts: BroadcastRecord[] = []; + const baselines: BaselineRecord[] = []; + const runtime = new SessionRuntime({ + session: sdk.session, + legacyApproval, + broadcast: (event, data, webviewId) => broadcasts.push({ event, data, webviewId }), + captureBaseline: (session, filePath, webviewIds) => { + baselines.push({ session, filePath, webviewIds }); + }, + log: () => undefined, + }); + runtime.subscribe("view-1"); + return { runtime, sdk, broadcasts, baselines }; +} + +function streamData(records: readonly BroadcastRecord[]): unknown[] { + return records.filter((record) => record.event === Events.StreamEvent).map((record) => record.data); +} + +function turnStarted(): Event { + return { + type: "turn.started", + sessionId: "session-1", + agentId: "main", + turnId: 7, + origin: { kind: "user" }, + }; +} + +function turnEnded( + reason: "completed" | "cancelled" | "failed", + error?: Extract["error"], +): Event { + return { + type: "turn.ended", + sessionId: "session-1", + agentId: "main", + turnId: 7, + reason, + error, + }; +} + +describe("session runtime (adapts one SDK session for subscribed Webviews)", () => { + it("announces the current context usage to a subscribed Webview", async () => { + const { runtime, broadcasts } = createRuntime(); + + await runtime.announceStatus("view-1"); + + expect(streamData(broadcasts)).toContainEqual({ + type: "StatusUpdate", + payload: { + thinking_effort: "off", + plan_mode: false, + context_usage: 0, + }, + _sessionId: "session-1", + }); + }); + + it("renders a host-only command without making it a forkable core turn", () => { + const { runtime, broadcasts } = createRuntime(); + + runtime.beginHostAction("/clear"); + runtime.emitHostText("The context has been cleared."); + runtime.completeHostAction(); + + expect(streamData(broadcasts)).toEqual([ + { + type: "TurnBegin", + payload: { user_input: "/clear", forkable: false }, + _sessionId: "session-1", + }, + { type: "StepBegin", payload: { n: 1 }, _sessionId: "session-1" }, + { + type: "ContentPart", + payload: { type: "text", text: "The context has been cleared." }, + _sessionId: "session-1", + }, + { + type: "stream_complete", + result: { status: "finished" }, + _sessionId: "session-1", + }, + ]); + }); + + it("cancels a long-running host action and ignores its late completion", async () => { + const { runtime, sdk, broadcasts } = createRuntime(); + const actionId = runtime.beginHostAction("/init"); + + await runtime.cancel(); + runtime.emitHostText("AGENTS.md has been generated.", actionId); + runtime.completeHostAction("finished", actionId); + + expect(sdk.cancelCount()).toBe(1); + expect(sdk.cancelCompactionCount()).toBe(1); + expect(streamData(broadcasts)).toContainEqual({ + type: "stream_complete", + result: { status: "cancelled" }, + _sessionId: "session-1", + }); + expect(JSON.stringify(streamData(broadcasts))).not.toContain("has been generated"); + }); + + it("does not let a cancelled action finish a newer host command", async () => { + const { runtime, broadcasts } = createRuntime(); + const initAction = runtime.beginHostAction("/init"); + await runtime.cancel(); + const clearAction = runtime.beginHostAction("/clear"); + + runtime.emitHostText("late init result", initAction); + runtime.completeHostAction("finished", initAction); + + expect(runtime.isBusy).toBe(true); + runtime.emitHostText("The context has been cleared.", clearAction); + runtime.completeHostAction("finished", clearAction); + expect(runtime.isBusy).toBe(false); + expect(JSON.stringify(streamData(broadcasts))).not.toContain("late init result"); + }); + + it("waits for the cancelled turn to settle before running an exclusive operation", async () => { + const { runtime, sdk } = createRuntime(); + const prompt = runtime.prompt("keep working"); + sdk.emit(turnStarted()); + let operationStarted = false; + + const operation = runtime.runExclusiveAfterCancelling(async () => { + operationStarted = true; + return "forked"; + }); + await Promise.resolve(); + await Promise.resolve(); + + expect(sdk.cancelCount()).toBe(1); + expect(operationStarted).toBe(false); + expect(runtime.isBusy).toBe(true); + + sdk.emit(turnEnded("cancelled")); + + await expect(prompt).resolves.toEqual({ status: "cancelled" }); + await expect(operation).resolves.toBe("forked"); + expect(operationStarted).toBe(true); + expect(runtime.isBusy).toBe(false); + }); + + it("keeps a public turn action attached to its original slash input", async () => { + const { runtime, sdk, broadcasts } = createRuntime(); + + const result = runtime.runTurnAction("/skill:review carefully", async () => { + sdk.emit(turnStarted()); + sdk.emit(turnEnded("completed")); + }); + + await expect(result).resolves.toEqual({ status: "finished" }); + expect(streamData(broadcasts)).toContainEqual({ + type: "TurnBegin", + payload: { user_input: "/skill:review carefully" }, + _sessionId: "session-1", + }); + }); + + it("converts legacy media keys when a prompt crosses the SDK boundary", async () => { + const { runtime, sdk } = createRuntime(); + const completion = runtime.prompt([ + { type: "text", text: "Describe these files" }, + { type: "image_url", image_url: { url: "data:image/png;base64,AA", id: "image-1" } }, + { type: "video_url", video_url: { url: "file:///workspace/demo.mp4", id: "video-1" } }, + ]); + + sdk.emit(turnStarted()); + sdk.emit(turnEnded("completed")); + await completion; + + expect(sdk.promptInputs).toEqual([ + [ + { type: "text", text: "Describe these files" }, + { type: "image_url", imageUrl: { url: "data:image/png;base64,AA", id: "image-1" } }, + { type: "video_url", videoUrl: { url: "file:///workspace/demo.mp4", id: "video-1" } }, + ], + ]); + }); + + it("broadcasts assistant text when the SDK streams a text delta", () => { + const { sdk, broadcasts } = createRuntime(); + + sdk.emit({ + type: "assistant.delta", + sessionId: "session-1", + agentId: "main", + turnId: 7, + delta: "Implemented", + }); + + expect(streamData(broadcasts)).toContainEqual({ + type: "ContentPart", + payload: { type: "text", text: "Implemented" }, + _sessionId: "session-1", + }); + }); + + it("broadcasts model thinking when the SDK streams a thinking delta", () => { + const { sdk, broadcasts } = createRuntime(); + + sdk.emit({ + type: "thinking.delta", + sessionId: "session-1", + agentId: "main", + turnId: 7, + delta: "Checking the edge case", + }); + + expect(streamData(broadcasts)).toContainEqual({ + type: "ContentPart", + payload: { type: "think", think: "Checking the edge case" }, + _sessionId: "session-1", + }); + }); + + it("broadcasts a legacy tool call when an SDK tool starts", () => { + const { sdk, broadcasts } = createRuntime(); + + sdk.emit({ + type: "tool.call.started", + sessionId: "session-1", + agentId: "main", + turnId: 7, + toolCallId: "tool-1", + name: "Read", + args: { path: "src/index.ts" }, + }); + + expect(streamData(broadcasts)).toContainEqual({ + type: "ToolCall", + payload: { + type: "function", + id: "tool-1", + function: { name: "ReadFile", arguments: '{"path":"src/index.ts"}' }, + }, + _sessionId: "session-1", + }); + }); + + it.each([ + ["completed", "finished"], + ["cancelled", "cancelled"], + ] as const)( + "emits one stream completion when a turn ends as %s", + async (reason, expectedStatus) => { + const { runtime, sdk, broadcasts } = createRuntime(); + const completion = runtime.prompt("hello"); + sdk.emit(turnStarted()); + + sdk.emit(turnEnded(reason)); + sdk.emit(turnEnded(reason)); + + await expect(completion).resolves.toEqual({ status: expectedStatus }); + expect( + streamData(broadcasts).filter( + (event) => + typeof event === "object" && + event !== null && + "type" in event && + event.type === "stream_complete", + ), + ).toEqual([ + { + type: "stream_complete", + result: { status: expectedStatus }, + _sessionId: "session-1", + }, + ]); + }, + ); + + it("emits one error when a failed turn terminal is repeated", async () => { + const { runtime, sdk, broadcasts } = createRuntime(); + const completion = runtime.prompt("hello"); + const error = { + code: "provider.api_error" as const, + message: "Provider rejected the request", + retryable: true, + }; + sdk.emit(turnStarted()); + + sdk.emit(turnEnded("failed", error)); + sdk.emit(turnEnded("failed", error)); + + await expect(completion).resolves.toEqual({ status: "failed" }); + expect( + streamData(broadcasts).filter( + (event) => + typeof event === "object" && event !== null && "type" in event && event.type === "error", + ), + ).toHaveLength(1); + }); + + it("suppresses the trailing SDK error when a failed terminal already reported the same error", async () => { + const { runtime, sdk, broadcasts } = createRuntime(); + const completion = runtime.prompt("hello"); + const error = { + code: "provider.api_error" as const, + message: "Provider rejected the request", + retryable: true, + }; + sdk.emit(turnStarted()); + + sdk.emit(turnEnded("failed", error)); + sdk.emit({ + type: "error", + sessionId: "session-1", + agentId: "main", + ...error, + }); + + await completion; + expect( + streamData(broadcasts).filter( + (event) => + typeof event === "object" && event !== null && "type" in event && event.type === "error", + ), + ).toHaveLength(1); + }); + + it("reports a preflight error when SDK prompt setup throws before turn start", async () => { + const { runtime, sdk, broadcasts } = createRuntime(); + sdk.rejectNextPrompt(new Error("Unable to initialize provider")); + + await expect(runtime.prompt("hello")).resolves.toEqual({ status: "failed" }); + + expect(streamData(broadcasts)).toContainEqual({ + type: "error", + code: "internal", + message: "Internal error occurred.", + detail: "Unable to initialize provider", + phase: "preflight", + _sessionId: "session-1", + }); + }); + + it("requests SDK cancellation when the active response is stopped", async () => { + const { runtime, sdk } = createRuntime(); + void runtime.prompt("hello"); + + await runtime.cancel(); + + expect(sdk.cancelCount()).toBe(1); + }); + + it("still reaches the SDK cancel when the host lost track of active work", async () => { + const { runtime, sdk } = createRuntime(); + + await runtime.cancel(); + + expect(sdk.cancelCount()).toBe(1); + }); + + it("converts legacy media keys when steering an active response", async () => { + const { runtime, sdk } = createRuntime(); + void runtime.prompt("hello"); + + await runtime.steer([ + { type: "text", text: "Use this instead" }, + { type: "image_url", image_url: { url: "file:///workspace/new.png" } }, + ]); + + expect(sdk.steerInputs).toEqual([ + [ + { type: "text", text: "Use this instead" }, + { type: "image_url", imageUrl: { url: "file:///workspace/new.png" } }, + ], + ]); + }); + + it("echoes a successful steer into the subscribed Webview", async () => { + const { runtime, broadcasts } = createRuntime(); + runtime.subscribe("view-a"); + + await runtime.steer("Use this instead"); + + expect(streamData(broadcasts)).toContainEqual({ + type: "SteerInput", + payload: { user_input: "Use this instead" }, + _sessionId: "session-1", + }); + }); + + it.each([ + ["approve", { decision: "approved" }], + ["approve_for_session", { decision: "approved", scope: "session" }], + ["reject", { decision: "rejected" }], + ] as const)("resolves SDK approval when the Webview responds with %s", async (response, expected) => { + const { runtime, sdk, broadcasts } = createRuntime(); + const pending = sdk.requestApproval({ + toolCallId: "tool-1", + toolName: "Bash", + action: "Run command", + display: { kind: "command", command: "pnpm test" }, + }); + const request = streamData(broadcasts).find( + (event) => + typeof event === "object" && + event !== null && + "type" in event && + event.type === "ApprovalRequest", + ) as { payload: { id: string } }; + + expect(runtime.respondApproval(request.payload.id, response)).toBe(true); + await expect(pending).resolves.toEqual(expected); + }); + + it("forwards SDK approval requests to the Webview in legacy yolo mode", async () => { + const { runtime, sdk, broadcasts } = createRuntime({ yolo: true, afk: false }); + const pending = sdk.requestApproval({ + toolCallId: "tool-yolo", + toolName: "Bash", + action: "Run command", + display: { kind: "command", command: "pnpm test" }, + }); + const request = streamData(broadcasts).find( + (event) => + typeof event === "object" && + event !== null && + "type" in event && + event.type === "ApprovalRequest", + ) as { payload: { id: string } }; + + expect(runtime.respondApproval(request.payload.id, "approve")).toBe(true); + await expect(pending).resolves.toEqual({ decision: "approved" }); + }); + + it("restores core permission when a legacy flag cannot be persisted", async () => { + const { runtime, sdk } = createRuntime(); + sdk.rejectNextMetadataUpdate(new Error("state is read-only")); + + await expect(runtime.toggleLegacyApproval("afk")).rejects.toThrow("state is read-only"); + + expect(sdk.setPermissions).toEqual(["auto", "manual"]); + expect(runtime.legacyApprovalFlags).toEqual({ yolo: false, afk: false }); + }); + + it("resolves an SDK question when the Webview submits answers", async () => { + const { runtime, sdk, broadcasts } = createRuntime(); + const pending = sdk.requestQuestion({ + toolCallId: "question-1", + questions: [ + { + question: "Choose a target", + header: "Target", + options: [{ label: "Tests", description: "Run focused tests" }], + multiSelect: false, + }, + ], + }); + const request = streamData(broadcasts).find( + (event) => + typeof event === "object" && + event !== null && + "type" in event && + event.type === "QuestionRequest", + ) as { payload: { id: string } }; + + expect(runtime.respondQuestion(request.payload.id, { "Choose a target": "Tests" })).toBe(true); + await expect(pending).resolves.toEqual({ answers: { "Choose a target": "Tests" } }); + }); + + it("keeps SDK questions interactive in legacy yolo mode", async () => { + const { runtime, sdk, broadcasts } = createRuntime({ yolo: true, afk: false }); + const pending = sdk.requestQuestion({ + toolCallId: "question-yolo", + questions: [ + { + question: "Continue?", + options: [{ label: "Yes" }], + multiSelect: false, + }, + ], + }); + const request = streamData(broadcasts).find( + (event) => + typeof event === "object" && + event !== null && + "type" in event && + event.type === "QuestionRequest", + ) as { payload: { id: string } }; + + expect(runtime.respondQuestion(request.payload.id, { "Continue?": "Yes" })).toBe(true); + await expect(pending).resolves.toEqual({ answers: { "Continue?": "Yes" } }); + }); + + it("cancels a pending SDK approval when the session closes", async () => { + const { runtime, sdk } = createRuntime(); + const pending = sdk.requestApproval({ + toolCallId: "tool-1", + toolName: "Bash", + action: "Run command", + display: { kind: "command", command: "pnpm test" }, + }); + + await runtime.close(); + + await expect(pending).resolves.toEqual({ + decision: "cancelled", + feedback: "Session closed", + }); + }); + + it("cancels a pending SDK question when the session closes", async () => { + const { runtime, sdk } = createRuntime(); + const pending = sdk.requestQuestion({ + questions: [ + { question: "Continue?", options: [{ label: "Yes" }], multiSelect: false }, + ], + }); + + await runtime.close(); + + await expect(pending).resolves.toBeNull(); + }); + + it("fans out one SDK subscription to every Webview attached to the session", () => { + const { runtime, sdk, broadcasts } = createRuntime(); + runtime.subscribe("view-2"); + + sdk.emit({ + type: "assistant.delta", + sessionId: "session-1", + agentId: "main", + turnId: 7, + delta: "Shared update", + }); + + expect(sdk.subscriptionCount()).toBe(1); + expect(sdk.handlerInstallations).toEqual({ approval: 1, question: 1 }); + expect(broadcasts.map((record) => record.webviewId)).toEqual(["view-1", "view-2"]); + }); + + it.each(["Write", "Edit"] as const)( + "captures the original file when %s starts with a path", + (name) => { + const { sdk, baselines } = createRuntime(); + + sdk.emit({ + type: "tool.call.started", + sessionId: "session-1", + agentId: "main", + turnId: 7, + toolCallId: "tool-1", + name, + args: { path: "src/index.ts" }, + }); + + expect(baselines).toEqual([ + { + session: { + id: "session-1", + workDir: "/workspace", + metadata: { source: "vscode-test" }, + }, + filePath: "src/index.ts", + webviewIds: ["view-1"], + }, + ]); + }, + ); + + it.each([ + ["Read", { path: "src/index.ts" }], + ["Write", {}], + ["Edit", { path: "" }], + ] as const)("does not capture a baseline when %s receives non-write input %#", (name, args) => { + const { sdk, baselines } = createRuntime(); + + sdk.emit({ + type: "tool.call.started", + sessionId: "session-1", + agentId: "main", + turnId: 7, + toolCallId: "tool-1", + name, + args, + }); + + expect(baselines).toEqual([]); + }); +}); diff --git a/apps/vscode/test/settings-store.test.ts b/apps/vscode/test/settings-store.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..38af845f2f7059f5105ed6031d5176a2c9f22e34 --- /dev/null +++ b/apps/vscode/test/settings-store.test.ts @@ -0,0 +1,573 @@ +/** + * Scenario: Webview state crosses the VS Code bridge during settings changes, MCP edits, and chat failures. + * Responsibilities: model metadata and selections remain provider-aware; MCP edits stay lossless; chat errors recover visibly. + * Wiring: the real Zustand store and MCP bridge; settings saves, toast, and the VS Code messaging API are the only replaced boundaries. + * Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts test/settings-store.test.ts + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { MCP_SECRET_MASK } from "../shared/legacy-sdk"; + +const boundary = vi.hoisted(() => ({ + saveConfig: vi.fn(), + streamChat: vi.fn(), + abortChat: vi.fn(), + trackFiles: vi.fn(), + toastError: vi.fn(), + toastWarning: vi.fn(), +})); + +vi.mock("@/services", () => ({ + bridge: { + saveConfig: boundary.saveConfig, + streamChat: boundary.streamChat, + abortChat: boundary.abortChat, + trackFiles: boundary.trackFiles, + }, +})); +vi.mock("@/components/ui/sonner", () => ({ + toast: { error: boundary.toastError, warning: boundary.toastWarning }, +})); + +import { + getMediaFallbackModel, + getModelThinkingMode, + groupModelsByProvider, + requiresManagedProviderLogin, + useSettingsStore, +} from "../webview-ui/src/stores/settings.store"; +import { useChatStore } from "../webview-ui/src/stores/chat.store"; + +const MODELS = [ + { id: "plain", name: "Plain", provider: "managed:kimi-code", capabilities: [] }, + { + id: "reasoning", + name: "Reasoning", + provider: "managed:kimi-code", + capabilities: ["thinking"], + support_efforts: ["low", "high"], + default_effort: "high", + }, + { id: "always", name: "Always", provider: "managed:kimi-code", capabilities: ["always_thinking"] }, +]; + +beforeEach(() => { + boundary.saveConfig.mockReset(); + boundary.streamChat.mockReset(); + boundary.streamChat.mockResolvedValue({ done: false }); + boundary.abortChat.mockReset(); + boundary.abortChat.mockResolvedValue({ aborted: true }); + boundary.trackFiles.mockReset(); + boundary.toastError.mockReset(); + boundary.toastWarning.mockReset(); + useSettingsStore.getState().initModels(MODELS, "plain", false); + useChatStore.setState({ + sessionId: null, + messages: [], + isStreaming: false, + isCompacting: false, + handshakeReceived: false, + draftMedia: [], + lastStatus: null, + tokenUsage: { input_other: 0, output: 0, input_cache_read: 0, input_cache_creation: 0 }, + activeTokenUsage: { input_other: 0, output: 0, input_cache_read: 0, input_cache_creation: 0 }, + pendingInput: null, + queue: [], + pendingQuestion: null, + planMode: false, + }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("Webview model settings persistence", () => { + it("persists the selected alias when display names collide across providers", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels([ + { id: "openai/shared", name: "Shared", provider: "openai", capabilities: [] }, + { id: "proxy/shared", name: "Shared", provider: "company-proxy", capabilities: [] }, + ], "openai/shared", false); + + useSettingsStore.getState().updateModel("proxy/shared"); + + expect(boundary.saveConfig).toHaveBeenCalledWith({ + model: "proxy/shared", + thinking: false, + effort: "off", + effortChanged: false, + }); + }); + + it("rolls back the optimistic model selection when saving fails", async () => { + let rejectSave!: (error: Error) => void; + boundary.saveConfig.mockReturnValue(new Promise((_resolve, reject) => { + rejectSave = reject; + })); + + useSettingsStore.getState().updateModel("reasoning"); + expect(useSettingsStore.getState()).toMatchObject({ + currentModel: "reasoning", + thinkingEffort: "off", + }); + + rejectSave(new Error("config.toml is read-only")); + await vi.waitFor(() => { + expect(useSettingsStore.getState()).toMatchObject({ + currentModel: "plain", + thinkingEffort: "off", + }); + }); + expect(boundary.toastError).toHaveBeenCalledWith( + "Failed to save model settings: config.toml is read-only", + ); + }); + + it("does not let an older failed save overwrite a newer selection", async () => { + let rejectFirst!: (error: Error) => void; + boundary.saveConfig + .mockReturnValueOnce(new Promise((_resolve, reject) => { + rejectFirst = reject; + })) + .mockResolvedValueOnce({ ok: true }); + + useSettingsStore.getState().updateModel("reasoning"); + useSettingsStore.getState().updateModel("always"); + rejectFirst(new Error("older request failed")); + await Promise.resolve(); + + expect(useSettingsStore.getState().currentModel).toBe("always"); + expect(boundary.toastError).not.toHaveBeenCalled(); + }); +}); + +describe("Webview model metadata", () => { + it("keeps same-named models in separate provider groups", () => { + const groups = groupModelsByProvider([ + { id: "kimi/shared", name: "Shared", provider: "managed:kimi-code", capabilities: [] }, + { id: "proxy/shared", name: "Shared", provider: "company-proxy", capabilities: [] }, + ]); + + expect(groups.map((group) => ({ + provider: group.provider, + label: group.label, + models: group.models.map((model) => model.id), + }))).toEqual([ + { provider: "company-proxy", label: "company-proxy", models: ["proxy/shared"] }, + { provider: "managed:kimi-code", label: "Kimi Code", models: ["kimi/shared"] }, + ]); + }); + + it("offers a thinking toggle when a model declares adaptive thinking", () => { + expect(getModelThinkingMode({ + id: "anthropic/claude", + name: "Claude", + provider: "anthropic", + capabilities: [], + adaptive_thinking: true, + })).toBe("switch"); + }); + + it("prefers a compatible model from the current provider for media fallback", () => { + const current = { + id: "openai/text", + name: "Text", + provider: "openai", + capabilities: [], + }; + const fallback = getMediaFallbackModel([ + { id: "other/vision", name: "Vision A", provider: "other", capabilities: ["image_in"] }, + { id: "openai/vision", name: "Vision B", provider: "openai", capabilities: ["image_in"] }, + ], current); + + expect(fallback?.id).toBe("openai/vision"); + }); + + it("does not require Kimi login when the default model uses a custom provider", () => { + expect(requiresManagedProviderLogin([ + { id: "local/model", name: "Local", provider: "local", capabilities: [] }, + ], "local/model", false)).toBe(false); + }); + + it("requires Kimi login when the default model uses the managed provider", () => { + expect(requiresManagedProviderLogin([ + { id: "kimi/model", name: "Kimi", provider: "managed:kimi-code", capabilities: [] }, + ], "kimi/model", false)).toBe(true); + }); +}); + +describe("Webview MCP update bridge", () => { + it("sends a lossless structured MCP edit request to the extension host", async () => { + const posted: unknown[] = []; + let receiveMessage: ((event: { data: unknown }) => void) | undefined; + vi.stubGlobal("document", { + body: { getAttribute: () => "mcp-test-view" }, + }); + vi.stubGlobal("window", { + addEventListener: (_type: string, listener: (event: { data: unknown }) => void) => { + receiveMessage = listener; + }, + }); + vi.stubGlobal("acquireVsCodeApi", () => ({ + postMessage: (message: { id: string }) => { + posted.push(message); + queueMicrotask(() => receiveMessage?.({ data: { id: message.id, result: [] } })); + }, + getState: () => undefined, + setState: () => undefined, + })); + vi.resetModules(); + const { bridge } = await import("../webview-ui/src/services/bridge"); + + await bridge.updateMCPServer("old-name", { + name: "new-name", + transport: "stdio", + command: "C:\\Program Files\\Example MCP\\server.exe", + args: ["--config", "C:\\Users\\Example User\\mcp config.json"], + env: { SERVICE_TOKEN: MCP_SECRET_MASK, DEBUG: "1" }, + }); + + expect(posted).toEqual([ + expect.objectContaining({ + method: "updateMCPServer", + webviewId: "mcp-test-view", + params: { + originalName: "old-name", + server: { + name: "new-name", + transport: "stdio", + command: "C:\\Program Files\\Example MCP\\server.exe", + args: ["--config", "C:\\Users\\Example User\\mcp config.json"], + env: { SERVICE_TOKEN: MCP_SECRET_MASK, DEBUG: "1" }, + }, + }, + }), + ]); + }); +}); + +describe("Webview chat error recovery", () => { + it("stops the pending state and keeps the input available when session setup fails", () => { + useChatStore.getState().sendMessage("retry this request"); + + useChatStore.getState().processEvent({ + type: "error", + code: "session.state_invalid", + message: "Session data is invalid.", + detail: "state.json: Unexpected token at line 4", + phase: "preflight", + }); + + expect(useChatStore.getState()).toMatchObject({ + isStreaming: false, + isCompacting: false, + pendingInput: { content: "retry this request", model: "plain" }, + }); + }); + + it("stops the response and retains provider detail when a running turn fails", () => { + useChatStore.getState().sendMessage("start a turn"); + useChatStore.getState().processEvent({ + type: "TurnBegin", + payload: { user_input: "start a turn" }, + }); + useChatStore.getState().processEvent({ type: "StepBegin", payload: { n: 1 } }); + + useChatStore.getState().processEvent({ + type: "error", + code: "provider.api_error", + message: "Service temporarily unavailable.", + detail: "HTTP 400: function name is invalid", + phase: "runtime", + }); + + expect(useChatStore.getState().isStreaming).toBe(false); + expect(useChatStore.getState().messages.at(-1)?.inlineError).toEqual({ + code: "provider.api_error", + message: "Service temporarily unavailable.", + detail: "HTTP 400: function name is invalid", + }); + }); +}); + +describe("Webview thinking mode parity with the TUI", () => { + it("derives thinking modes from metadata only, mirroring the TUI rules", () => { + const base = { id: "m", name: "M", provider: "p", capabilities: [] as string[] }; + expect(getModelThinkingMode({ ...base, capabilities: ["thinking"], support_efforts: ["low", "high"] })).toBe("effort"); + expect(getModelThinkingMode({ ...base, capabilities: ["always_thinking"] })).toBe("always"); + expect(getModelThinkingMode({ ...base, capabilities: ["thinking"] })).toBe("switch"); + expect(getModelThinkingMode({ ...base, adaptive_thinking: true })).toBe("switch"); + expect(getModelThinkingMode({ ...base, name: "Kimi Thinking Pro" })).toBe("none"); + expect(getModelThinkingMode(base)).toBe("none"); + }); +}); + +describe("Webview thinking effort parity with the TUI", () => { + it("resolves a boolean \"on\" to the model default for effort-capable models", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", false); + + useSettingsStore.getState().selectThinkingEffort("on"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("high"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ model: "reasoning", thinking: true, effort: "high" }); + }); + + it("prefers the persisted configured effort when resolving \"on\"", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", false); + useSettingsStore.setState({ defaultThinkingEffort: "low" }); + + useSettingsStore.getState().selectThinkingEffort("on"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("low"); + }); + + it("keeps \"on\" for genuine boolean models", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels([ + { id: "bool", name: "Bool", provider: "openai", capabilities: ["thinking"] }, + ], "bool", false); + + useSettingsStore.getState().selectThinkingEffort("on"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("on"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ model: "bool", thinking: true, effort: "on" }); + }); + + it("persists disabling thinking with thinking false", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", true); + + useSettingsStore.getState().selectThinkingEffort("off"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("off"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ model: "reasoning", thinking: false, effort: "off" }); + }); + + it("rejects \"off\" for always-on effort models", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels([ + { id: "always-effort", name: "AE", provider: "openai", capabilities: ["always_thinking"], support_efforts: ["low", "high"] }, + ], "always-effort", true); + const previous = useSettingsStore.getState().thinkingEffort; + boundary.saveConfig.mockClear(); + + useSettingsStore.getState().selectThinkingEffort("off"); + + expect(useSettingsStore.getState().thinkingEffort).toBe(previous); + expect(boundary.saveConfig).not.toHaveBeenCalled(); + }); + + it("rejects efforts outside support_efforts", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", false); + const previous = useSettingsStore.getState().thinkingEffort; + boundary.saveConfig.mockClear(); + + useSettingsStore.getState().selectThinkingEffort("ultra"); + + expect(useSettingsStore.getState().thinkingEffort).toBe(previous); + expect(boundary.saveConfig).not.toHaveBeenCalled(); + }); + + it("skips the config write when re-confirming the current effort", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", true); + expect(useSettingsStore.getState().thinkingEffort).toBe("high"); + boundary.saveConfig.mockClear(); + + useSettingsStore.getState().selectThinkingEffort("high"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("high"); + expect(boundary.saveConfig).not.toHaveBeenCalled(); + }); + + it("seeds the top tier when it is the model's delivered default", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", false); + + useSettingsStore.getState().selectThinkingEffort("high"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("high"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ model: "reasoning", thinking: true, effort: "high" }); + expect(useSettingsStore.getState().defaultThinkingEffort).toBe("high"); + }); + + it("does not seed a pick above the model's delivered default", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels([ + { + id: "reasoning", + name: "Reasoning", + provider: "managed:kimi-code", + capabilities: ["thinking"], + support_efforts: ["low", "high", "max"], + default_effort: "low", + }, + ], "reasoning", false); + + useSettingsStore.getState().selectThinkingEffort("high"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("high"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ model: "reasoning", thinking: true, effort: "high" }); + expect(useSettingsStore.getState().defaultThinkingEffort).toBeUndefined(); + }); + + it("does not seed the top tier when the model declares no default", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels([ + { + id: "reasoning", + name: "Reasoning", + provider: "managed:kimi-code", + capabilities: ["thinking"], + support_efforts: ["low", "high"], + }, + ], "reasoning", false); + + useSettingsStore.getState().selectThinkingEffort("high"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("high"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ model: "reasoning", thinking: true, effort: "high" }); + expect(useSettingsStore.getState().defaultThinkingEffort).toBeUndefined(); + }); + + const SWITCH_MODELS = [ + { + id: "seeded", + name: "Seeded", + provider: "managed:kimi-code", + capabilities: ["thinking"], + support_efforts: ["low", "medium"], + default_effort: "medium", + }, + { + id: "max-default", + name: "Max Default", + provider: "managed:kimi-code", + capabilities: ["thinking"], + support_efforts: ["low", "max"], + default_effort: "max", + }, + ]; + + it("updates the seed when a model switch persists the derived effort", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(SWITCH_MODELS, "seeded", true, "medium"); + + // "medium" is unsupported here, so the switch derives the model default + // "max"; with the delivered default at the top tier the host persists it. + useSettingsStore.getState().updateModel("max-default"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("max"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ + model: "max-default", + thinking: true, + effort: "max", + effortChanged: true, + }); + expect(useSettingsStore.getState().defaultThinkingEffort).toBe("max"); + }); + + it("rolls the seed back when the model-switch save fails", async () => { + let rejectSave!: (error: Error) => void; + boundary.saveConfig.mockReturnValue(new Promise((_resolve, reject) => { + rejectSave = reject; + })); + useSettingsStore.getState().initModels(SWITCH_MODELS, "seeded", true, "medium"); + + useSettingsStore.getState().updateModel("max-default"); + expect(useSettingsStore.getState().defaultThinkingEffort).toBe("max"); + + rejectSave(new Error("config.toml is read-only")); + await vi.waitFor(() => { + expect(useSettingsStore.getState().defaultThinkingEffort).toBe("medium"); + }); + }); + + it("leaves the seed alone when the switch re-confirms the active effort", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + // No persisted effort: the seed starts undefined and the session derives + // "max" from the model default. + useSettingsStore.getState().initModels([ + ...SWITCH_MODELS, + { + id: "max-default-b", + name: "Max Default B", + provider: "managed:kimi-code", + capabilities: ["thinking"], + support_efforts: ["low", "max"], + default_effort: "max", + }, + ], "max-default", true); + + // The derived effort equals the active one, so the host leaves the stored + // preference untouched — the seed must not invent one either. + useSettingsStore.getState().updateModel("max-default-b"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("max"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ + model: "max-default-b", + thinking: true, + effort: "max", + effortChanged: false, + }); + expect(useSettingsStore.getState().defaultThinkingEffort).toBeUndefined(); + }); + + it("seeds future sessions with a persisted non-top effort", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", false); + + useSettingsStore.getState().selectThinkingEffort("low"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("low"); + expect(useSettingsStore.getState().defaultThinkingEffort).toBe("low"); + }); + + it("marks the effort as changed when a model switch resolves to a different effort", () => { + boundary.saveConfig.mockResolvedValue({ ok: true }); + useSettingsStore.getState().initModels(MODELS, "reasoning", true); + expect(useSettingsStore.getState().thinkingEffort).toBe("high"); + + useSettingsStore.getState().updateModel("always"); + + expect(useSettingsStore.getState().thinkingEffort).toBe("on"); + expect(boundary.saveConfig).toHaveBeenCalledWith({ model: "always", thinking: true, effort: "on", effortChanged: true }); + }); +}); + +describe("Webview mid-turn warnings", () => { + it("shows a non-terminal error as a toast without unlocking the composer", async () => { + useChatStore.getState().sendMessage("first message"); + useChatStore.getState().sendMessage("queued follow-up"); + expect(useChatStore.getState().isStreaming).toBe(true); + expect(useChatStore.getState().queue).toHaveLength(1); + + useChatStore.getState().processEvent({ + type: "error", + code: "internal", + message: "Internal error occurred.", + detail: "A response is already being generated for this session.", + phase: "runtime", + terminal: false, + }); + + // The turn is still running: nothing unlocks, nothing flushes, nothing is retried. + expect(boundary.toastWarning).toHaveBeenCalledWith("Internal error occurred."); + const state = useChatStore.getState(); + expect(state.isStreaming).toBe(true); + expect(state.queue).toHaveLength(1); + expect(state.pendingInput).not.toBeNull(); + expect(state.messages.at(-1)?.inlineError).toBeUndefined(); + + // The genuine terminal still completes the turn and flushes the queue. + useChatStore.getState().processEvent({ type: "stream_complete", result: { status: "finished" } }); + expect(useChatStore.getState().isStreaming).toBe(false); + await vi.waitFor(() => { + expect(boundary.streamChat).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/apps/vscode/test/vsix-package.test.ts b/apps/vscode/test/vsix-package.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd258e7a7b92ba2b265c20ebc33e4507b030d409 --- /dev/null +++ b/apps/vscode/test/vsix-package.test.ts @@ -0,0 +1,275 @@ +/** + * Scenario: maintainers package and inspect the VS Code extension on any host OS. + * Responsibilities: six-target argument handling, actionable failures, isolated + * dev state, manifest/resource hygiene, unresolved imports, and entry loading. + * Wiring: real Node packaging/verifier CLIs and filesystem; VSIX directory + * fixtures replace only the external Marketplace archive producer. + * Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/vsix-package.test.ts + */ +import { spawnSync } from 'node:child_process'; +import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +const appRoot = resolve(import.meta.dirname, '..'); +const packageScript = join(appRoot, 'scripts', 'vsix-package.mjs'); +const verifierScript = join(appRoot, 'scripts', 'vsix-verify.mjs'); +const prepareDevScript = join(appRoot, 'scripts', 'prepare-dev.mjs'); +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe('VSIX package CLI (target planning and validation)', () => { + it('plans all six supported targets when no target is supplied', async () => { + const outputDir = await makeTempDir('kimi-package-plan-'); + + const result = runNode(packageScript, ['--dry-run', '--out-dir', outputDir]); + + expect(result.status).toBe(0); + expect(result.stdout.match(/Would package /g)).toHaveLength(6); + expect(result.stdout).toContain('kimi-code-darwin-x64.vsix'); + expect(result.stdout).toContain('kimi-code-darwin-arm64.vsix'); + expect(result.stdout).toContain('kimi-code-linux-x64.vsix'); + expect(result.stdout).toContain('kimi-code-linux-arm64.vsix'); + expect(result.stdout).toContain('kimi-code-win32-x64.vsix'); + expect(result.stdout).toContain('kimi-code-win32-arm64.vsix'); + }); + + it('accepts a Windows ARM target when the output path contains spaces', async () => { + const root = await makeTempDir('kimi-package-windows-'); + const outputDir = join(root, 'output with spaces'); + + const result = runNode(packageScript, [ + '--', + '--dry-run', + '--target', + 'win32-arm64', + '--out-dir', + outputDir, + ]); + + expect(result.status).toBe(0); + expect(result.stdout.match(/Would package /g)).toHaveLength(1); + expect(result.stdout).toContain('kimi-code-win32-arm64.vsix'); + }); + + it('rejects an unknown target before a build starts', () => { + const result = runNode(packageScript, ['plan9-x64', '--dry-run']); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Unknown VSIX target "plan9-x64"'); + expect(result.stderr).toContain('win32-arm64'); + }); +}); + +describe('VSIX verifier CLI (package contract and failure details)', () => { + it('passes an unpacked Windows package when the entry is self-contained', async () => { + const fixture = await makeVsixFixture('win32-x64'); + + const result = runNode(verifierScript, [ + '--target', + 'win32-x64', + '--directory', + fixture, + ]); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('static audit and entry import smoke passed (package-only)'); + }); + + it('reports the expected target when the VSIX manifest has a different target', async () => { + const fixture = await makeVsixFixture('darwin-arm64'); + + const result = runNode(verifierScript, [ + '--target', + 'linux-x64', + '--directory', + fixture, + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('VSIX manifest target is darwin-arm64, expected linux-x64'); + }); + + it('names a missing required Webview resource', async () => { + const fixture = await makeVsixFixture('darwin-x64'); + await rm(join(fixture, 'extension', 'dist', 'webview.js')); + + const result = runNode(verifierScript, [ + '--target', + 'darwin-x64', + '--directory', + fixture, + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Required VSIX resource is missing: extension/dist/webview.js'); + }); + + it('reports a bare runtime dependency left in the extension bundle', async () => { + const fixture = await makeVsixFixture('linux-arm64'); + await writeFile( + join(fixture, 'extension', 'dist', 'extension.js'), + "import leftPad from 'left-pad';\nexport function activate() { return leftPad; }\n", + ); + + const result = runNode(verifierScript, [ + '--target', + 'linux-arm64', + '--directory', + fixture, + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Bare runtime dependency "left-pad"'); + }); + + it('does not mistake a bundled this.require(...) method call for a runtime dependency', async () => { + const fixture = await makeVsixFixture('darwin-arm64'); + await writeFile( + join(fixture, 'extension', 'dist', 'extension.js'), + 'class HostEnvironment {\n' + + ' require(field) { return this.info[field]; }\n' + + ' get osKind() { return this.require("osKind"); }\n' + + '}\n' + + 'export function activate() { return new HostEnvironment(); }\n', + ); + + const result = runNode(verifierScript, [ + '--target', + 'darwin-arm64', + '--directory', + fixture, + ]); + + expect(result.status).toBe(0); + }); + + it('rejects generated session state inside the package', async () => { + const fixture = await makeVsixFixture('win32-arm64'); + const stateDir = join(fixture, 'extension', 'runtime', 'profile'); + await mkdir(stateDir, { recursive: true }); + await writeFile(join(stateDir, 'session.json'), '{}'); + + const result = runNode(verifierScript, [ + '--target', + 'win32-arm64', + '--directory', + fixture, + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Forbidden package path segment "runtime"'); + }); + + it('rejects a persisted state directory inside the package', async () => { + const fixture = await makeVsixFixture('win32-x64'); + const stateDir = join(fixture, 'extension', 'state'); + await mkdir(stateDir, { recursive: true }); + await writeFile(join(stateDir, 'extension.json'), '{}'); + + const result = runNode(verifierScript, [ + '--target', + 'win32-x64', + '--directory', + fixture, + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Forbidden package path segment "state"'); + }); +}); + +describe('Extension Development Host setup (isolated local state)', () => { + it('creates the complete isolated directory layout for a debug launch', async () => { + const parent = await makeTempDir('kimi-dev-profile-'); + const baseDir = join(parent, 'vscode-extension-dev'); + + const result = runNode(prepareDevScript, ['--base-dir', baseDir]); + + expect(result.status).toBe(0); + await expect(readFile(join(baseDir, 'workspace', 'README.md'), 'utf8')).resolves.toContain( + 'Isolated Kimi Code extension development workspace', + ); + await expect(directoryExists(join(baseDir, 'user-data'))).resolves.toBe(true); + await expect(directoryExists(join(baseDir, 'extensions'))).resolves.toBe(true); + await expect(directoryExists(join(baseDir, 'kimi-home'))).resolves.toBe(true); + }); + + it('refuses to clear a directory without the dedicated safety suffix', async () => { + const unsafeDir = await makeTempDir('kimi-dev-unsafe-'); + await writeFile(join(unsafeDir, 'keep.txt'), 'keep'); + + const result = runNode(prepareDevScript, ['--base-dir', unsafeDir]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Refusing to reset unsafe development directory'); + await expect(readFile(join(unsafeDir, 'keep.txt'), 'utf8')).resolves.toBe('keep'); + }); +}); + +async function makeTempDir(prefix: string): Promise { + const dir = await mkdtemp(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +async function makeVsixFixture(target: string): Promise { + const root = await makeTempDir('kimi-vsix-fixture-'); + const extensionDir = join(root, 'extension'); + const distDir = join(extensionDir, 'dist'); + const resourcesDir = join(extensionDir, 'resources'); + await Promise.all([ + mkdir(distDir, { recursive: true }), + mkdir(resourcesDir, { recursive: true }), + ]); + + const packageJson = await readFile(join(appRoot, 'package.json'), 'utf8'); + await Promise.all([ + writeFile(join(root, '[Content_Types].xml'), ''), + writeFile( + join(root, 'extension.vsixmanifest'), + ``, + ), + writeFile(join(extensionDir, 'package.json'), packageJson), + writeFile(join(extensionDir, 'readme.md'), '# Fixture\n'), + writeFile(join(extensionDir, 'LICENSE.txt'), 'fixture license\n'), + writeFile( + join(distDir, 'extension.js'), + "/** @type {import('../types/index').Extension} */\nimport * as vscode from 'vscode';\nexport function activate() { return vscode; }\n", + ), + writeFile(join(distDir, 'webview.js'), 'globalThis.__kimiWebview = true;\n'), + writeFile(join(distDir, 'kimi-banner-dark.svg'), ''), + writeFile(join(distDir, 'kimi-banner-light.svg'), ''), + writeFile(join(distDir, 'kimi-logo.png'), 'fixture'), + writeFile(join(resourcesDir, 'kimi-icon-storefront.png'), 'fixture'), + writeFile(join(resourcesDir, 'kimi-icon.svg'), ''), + ]); + return root; +} + +function runNode(script: string, args: string[]) { + const result = spawnSync(process.execPath, [script, ...args], { + cwd: appRoot, + encoding: 'utf8', + env: { ...process.env, VSCE_PAT: '', OVSX_PAT: '' }, + }); + if (result.error !== undefined) throw result.error; + return { + status: result.status, + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + }; +} + +async function directoryExists(path: string): Promise { + try { + return (await stat(path)).isDirectory(); + } catch { + return false; + } +} diff --git a/apps/vscode/test/workspace-paths.test.ts b/apps/vscode/test/workspace-paths.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..191d35934d1d9e12ac6f686c3e6b1f1093d054bf --- /dev/null +++ b/apps/vscode/test/workspace-paths.test.ts @@ -0,0 +1,459 @@ +/** + * Scenario: Webview file paths and the selected working directory. + * Responsibilities: directory/search/open paths are scoped, normalized, and symlink-safe; + * editor mentions use relative paths inside the working directory and absolute paths outside. + * Wiring: real temporary local files plus the public handler/bridge surfaces; + * VS Code host APIs are the only stubbed boundary. + * Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/workspace-paths.test.ts + */ +import { mkdtemp, mkdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Event, Session } from "@moonshot-ai/kimi-code-sdk"; +import type * as vscode from "vscode"; +import { Methods } from "../shared/bridge"; +import { BridgeHandler } from "../src/bridge-handler"; +import { fileHandlers } from "../src/handlers/file.handler"; +import type { HandlerContext } from "../src/handlers/types"; +import { FileManager } from "../src/managers/file.manager"; +import { SessionRuntime } from "../src/runtime/session-runtime"; +import { areSameFsPath, isFsPathInsideOrEqual } from "../src/utils/fs-path"; +import { relativeWorkspacePath } from "../src/utils/workspace-path"; + +const vscodeHost = vi.hoisted(() => { + function normalizeUriPath(value: string): string { + const segments: string[] = []; + for (const segment of value.replaceAll("\\", "/").split("/")) { + if (!segment || segment === ".") continue; + if (segment === "..") segments.pop(); + else segments.push(segment); + } + return `/${segments.join("/")}`; + } + + class Uri { + readonly query: string; + + constructor( + readonly scheme: string, + readonly authority: string, + readonly path: string, + readonly fsPath: string, + query = "", + ) { + this.query = query; + } + + static file(fsPath: string): Uri { + return new Uri("file", "", normalizeUriPath(fsPath), fsPath); + } + + static remote(authority: string, fsPath: string): Uri { + return new Uri("vscode-remote", authority, normalizeUriPath(fsPath), fsPath); + } + + static joinPath(base: Uri, ...segments: string[]): Uri { + const path = normalizeUriPath(`${base.path}/${segments.join("/")}`); + const suffix = segments.join("/"); + const separator = base.fsPath.endsWith("/") ? "" : "/"; + return new Uri(base.scheme, base.authority, path, `${base.fsPath}${separator}${suffix}`); + } + + static from(parts: { scheme: string; path?: string; query?: string }): Uri { + const path = parts.path ?? ""; + return new Uri(parts.scheme, "", path, path, parts.query); + } + + toString(): string { + return `${this.scheme}://${this.authority}${this.path}${this.query ? `?${this.query}` : ""}`; + } + } + + class RelativePattern { + readonly baseUri: Uri; + readonly base: string; + + constructor(base: Uri, readonly pattern: string) { + this.baseUri = base; + this.base = base.fsPath; + } + } + + const readDirectory = vi.fn(); + const stat = vi.fn(); + const readFile = vi.fn(); + const findFiles = vi.fn(); + const executeCommand = vi.fn(); + const showWarningMessage = vi.fn(); + + return { + Uri, + RelativePattern, + readDirectory, + stat, + readFile, + findFiles, + executeCommand, + showWarningMessage, + workspaceFolders: [] as Array<{ uri: Uri }>, + }; +}); + +vi.mock("vscode", () => ({ + Uri: vscodeHost.Uri, + RelativePattern: vscodeHost.RelativePattern, + FileType: { Unknown: 0, File: 1, Directory: 2, SymbolicLink: 64 }, + QuickPickItemKind: { Separator: -1 }, + workspace: { + get workspaceFolders() { + return vscodeHost.workspaceFolders; + }, + fs: { + readDirectory: vscodeHost.readDirectory, + stat: vscodeHost.stat, + readFile: vscodeHost.readFile, + }, + findFiles: vscodeHost.findFiles, + createFileSystemWatcher: () => ({ + onDidChange: vi.fn(), + onDidCreate: vi.fn(), + onDidDelete: vi.fn(), + dispose: vi.fn(), + }), + getConfiguration: () => ({ get: (_key: string, fallback: unknown) => fallback }), + textDocuments: [], + }, + commands: { executeCommand: vscodeHost.executeCommand }, + window: { + activeTextEditor: undefined, + showWarningMessage: vscodeHost.showWarningMessage, + showQuickPick: vi.fn(), + showOpenDialog: vi.fn(), + }, +})); + +vi.mock("@moonshot-ai/kimi-code-sdk", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + createKimiHarness: () => ({ + homeDir: "/tmp/kimi-code-test-home", + close: vi.fn(), + }), + }; +}); + +let root: string; +let fileManager: FileManager; +let bridges: BridgeHandler[]; +let sessionRuntimes: SessionRuntime[]; +let extraRoots: string[]; + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), "kimi-vscode-workspace-paths-")); + vscodeHost.workspaceFolders.splice(0, vscodeHost.workspaceFolders.length, { uri: vscodeHost.Uri.file(root) }); + vscodeHost.stat.mockImplementation((uri: { fsPath: string }) => stat(uri.fsPath)); + vscodeHost.readFile.mockImplementation((uri: { fsPath: string }) => readFile(uri.fsPath)); + vscodeHost.findFiles.mockResolvedValue([]); + fileManager = new FileManager({} as never, vi.fn()); + bridges = []; + sessionRuntimes = []; + extraRoots = []; +}); + +afterEach(async () => { + await Promise.all(sessionRuntimes.map((runtime) => runtime.close())); + await Promise.all(bridges.map((bridge) => bridge.dispose())); + fileManager.dispose(); + vi.clearAllMocks(); + await Promise.all([root, ...extraRoots].map((directory) => rm(directory, { recursive: true, force: true }))); +}); + +describe("Webview workspace paths (selected-directory containment)", () => { + it("searches within the selected subdirectory using work-directory-relative results", async () => { + const workDir = join(root, "project", "subproject"); + const inside = join(workDir, "src", "inside.ts"); + const sibling = join(root, "project", "sibling.ts"); + await mkdir(join(workDir, "src"), { recursive: true }); + await Promise.all([writeFile(inside, "inside"), writeFile(sibling, "sibling")]); + vscodeHost.findFiles.mockResolvedValue([vscodeHost.Uri.file(inside), vscodeHost.Uri.file(sibling)]); + + const files = await getProjectFiles(workDir, { query: "inside" }); + + const include = vscodeHost.findFiles.mock.calls[0]?.[0] as InstanceType; + expect(include.baseUri.fsPath).toBe(workDir); + expect(files).toEqual([{ path: "src/inside.ts", name: "inside.ts", isDirectory: false }]); + }); + + it("maps engine suggestions without touching the local file search", async () => { + const workDir = join(root, "project"); + await mkdir(workDir); + const ctx = createContext(vscodeHost.Uri.file(workDir)); + const suggestFiles = vi.mocked(ctx.harness.suggestFiles); + suggestFiles.mockResolvedValue({ + items: [ + { path: "src/app.ts", name: "app.ts", kind: "file", matchPositions: [4, 5, 6] }, + { path: "src", name: "src", kind: "directory", matchPositions: [] }, + ], + truncated: false, + }); + + const files = await fileHandlers[Methods.GetProjectFiles]!({ query: "app" }, ctx); + + expect(suggestFiles).toHaveBeenCalledWith(workDir, { query: "app", limit: 20 }); + expect(vscodeHost.findFiles).not.toHaveBeenCalled(); + expect(files).toEqual([ + { path: "src/app.ts", name: "app.ts", isDirectory: false, matchPositions: [4, 5, 6] }, + { path: "src", name: "src", isDirectory: true, matchPositions: [] }, + ]); + }); + + it("degrades to an empty list when engine suggestions fail", async () => { + const workDir = join(root, "project"); + await mkdir(workDir); + const ctx = createContext(vscodeHost.Uri.file(workDir)); + vi.mocked(ctx.harness.suggestFiles).mockRejectedValue(new Error("engine down")); + + const files = await fileHandlers[Methods.GetProjectFiles]!({ query: "app" }, ctx); + + expect(files).toEqual([]); + expect(ctx.logError).toHaveBeenCalled(); + expect(vscodeHost.findFiles).not.toHaveBeenCalled(); + }); + + it("refuses to open a symlink whose target lies outside the selected working directory", async () => { + const workDir = join(root, "project"); + const outside = join(root, "outside.txt"); + await mkdir(workDir); + await writeFile(outside, "secret"); + await symlink(outside, join(workDir, "secret.txt")); + const ctx = createContext(vscodeHost.Uri.file(workDir)); + + const result = await fileHandlers[Methods.OpenFile]!({ filePath: "secret.txt" }, ctx); + + expect(result).toEqual({ ok: false }); + expect(vscodeHost.executeCommand).not.toHaveBeenCalled(); + }); + + it("omits an outside symlink when an SDK Write event requests baseline capture", async () => { + const workDir = join(root, "project"); + const outsideRoot = await mkdtemp(join(tmpdir(), "kimi-vscode-baseline-outside-")); + extraRoots.push(outsideRoot); + const outside = join(outsideRoot, "outside.txt"); + const linkedFile = join(workDir, "linked.txt"); + await mkdir(workDir); + await writeFile(outside, "secret"); + await symlink(outside, linkedFile); + const bridge = createBridge(); + let emit!: (event: Event) => void; + const session = { + id: "session-1", + workDir, + summary: { id: "session-1", workDir }, + setApprovalHandler: vi.fn(), + setQuestionHandler: vi.fn(), + onEvent(listener: (event: Event) => void) { + emit = listener; + return vi.fn(); + }, + close: vi.fn(), + } as unknown as Session; + const runtime = new SessionRuntime({ + session, + legacyApproval: { yolo: false, afk: false }, + broadcast: vi.fn(), + captureBaseline: (summary, filePath, webviewIds) => { + bridge.captureFileBaseline(summary, filePath, webviewIds); + }, + log: vi.fn(), + }); + sessionRuntimes.push(runtime); + + emit({ + type: "tool.call.started", + sessionId: "session-1", + agentId: "main", + turnId: 1, + toolCallId: "tool-1", + name: "Write", + args: { path: linkedFile }, + }); + + await expect(bridge.baselineManager.getChanges({ id: "session-1", workDir })).resolves.toEqual([]); + }); + + it("builds an editor mention relative to the selected working directory", async () => { + const workDir = join(root, "project", "subproject"); + const inside = join(workDir, "src", "inside.ts"); + await mkdir(join(workDir, "src"), { recursive: true }); + await writeFile(inside, "inside"); + const bridge = createBridge(); + await bridge.handle({ id: "set", method: Methods.SetWorkDir, params: { workDir } }, "view-1"); + + const mention = await bridge.getEditorMention( + "view-1", + vscodeHost.Uri.file(inside) as vscode.Uri, + emptySelection(), + ); + + expect(mention).toBe("@src/inside.ts"); + }); + + it("builds an absolute editor mention when the file is outside the selected working directory", async () => { + const workDir = join(root, "project", "subproject"); + const sibling = join(root, "project", "sibling.ts"); + await mkdir(workDir, { recursive: true }); + await writeFile(sibling, "sibling"); + const bridge = createBridge(); + await bridge.handle({ id: "set", method: Methods.SetWorkDir, params: { workDir } }, "view-1"); + + const mention = await bridge.getEditorMention( + "view-1", + vscodeHost.Uri.file(sibling) as vscode.Uri, + emptySelection(), + ); + + expect(mention).toBe(`@${sibling}`); + }); + + it("builds an absolute editor mention when the file is outside the workspace root", async () => { + const otherRoot = await mkdtemp(join(tmpdir(), "kimi-vscode-mention-outside-")); + extraRoots.push(otherRoot); + const outside = join(otherRoot, "App.java"); + await writeFile(outside, "class App {}"); + const bridge = createBridge(); + + const mention = await bridge.getEditorMention( + "view-1", + vscodeHost.Uri.file(outside) as vscode.Uri, + emptySelection(), + ); + + expect(mention).toBe(`@${outside}`); + }); + + it("quotes an absolute editor mention whose path contains spaces", async () => { + const otherRoot = await mkdtemp(join(tmpdir(), "kimi vscode mention space-")); + extraRoots.push(otherRoot); + const outside = join(otherRoot, "App.java"); + await writeFile(outside, "class App {}"); + const bridge = createBridge(); + + const mention = await bridge.getEditorMention( + "view-1", + vscodeHost.Uri.file(outside) as vscode.Uri, + emptySelection(), + ); + + expect(mention).toBe(`@"${outside}"`); + }); + + it("places the line range after the closing quote of a mention with spaces", async () => { + const workDir = join(root, "project"); + const inside = join(workDir, "some dir", "inside.ts"); + await mkdir(join(workDir, "some dir"), { recursive: true }); + await writeFile(inside, "inside"); + const bridge = createBridge(); + await bridge.handle({ id: "set", method: Methods.SetWorkDir, params: { workDir } }, "view-1"); + + const mention = await bridge.getEditorMention( + "view-1", + vscodeHost.Uri.file(inside) as vscode.Uri, + { isEmpty: false, start: { line: 2 }, end: { line: 4 } } as vscode.Selection, + ); + + expect(mention).toBe('@"some dir/inside.ts":3-5'); + }); + + it("omits an editor mention for a document that is not on the workspace file system", async () => { + const bridge = createBridge(); + const untitled = vscodeHost.Uri.from({ scheme: "untitled", path: "Untitled-1" }); + + const mention = await bridge.getEditorMention( + "view-1", + untitled as vscode.Uri, + emptySelection(), + ); + + expect(mention).toBeNull(); + }); + + it("rejects a selected working directory whose symlink target leaves the workspace", async () => { + const outside = await mkdtemp(join(tmpdir(), "kimi-vscode-outside-")); + extraRoots.push(outside); + const linkedWorkDir = join(root, "linked-project"); + await symlink(outside, linkedWorkDir); + const bridge = createBridge(); + + const result = await bridge.handle( + { id: "set", method: Methods.SetWorkDir, params: { workDir: linkedWorkDir } }, + "view-1", + ); + + expect(result).toEqual({ id: "set", result: { ok: false } }); + }); +}); + +describe("native workspace path comparison (Windows drive and UNC semantics)", () => { + it("treats slash and casing differences as the same Windows drive path", () => { + expect(areSameFsPath("C:\\Users\\Example User\\项目", "c:/users/example user/项目")).toBe(true); + }); + + it("keeps an in-share UNC child relative to its workspace", () => { + const rootUri = vscodeHost.Uri.file("\\\\Server\\Share\\Workspace"); + const childUri = vscodeHost.Uri.file("\\\\server\\share\\workspace\\src\\app.ts"); + + expect(relativeWorkspacePath(rootUri as vscode.Uri, childUri as vscode.Uri)).toBe("src/app.ts"); + }); + + it("rejects a UNC path from another share", () => { + expect( + isFsPathInsideOrEqual( + "\\\\Server\\Share\\Workspace", + "\\\\Server\\OtherShare\\Workspace\\app.ts", + ), + ).toBe(false); + }); +}); + +async function getProjectFiles(workDir: string, params: { query?: string }) { + return fileHandlers[Methods.GetProjectFiles]!(params, createContext(vscodeHost.Uri.file(workDir))); +} + +function createContext(workDirUri: InstanceType): HandlerContext { + return { + webviewId: "view-1", + workDir: workDirUri.fsPath, + workDirUri: workDirUri as vscode.Uri, + workspaceRoot: root, + workspaceRootUri: vscodeHost.workspaceFolders[0]!.uri as vscode.Uri, + workspaceState: {} as vscode.Memento, + requireWorkDir: () => workDirUri.fsPath, + requireWorkDirUri: () => workDirUri as vscode.Uri, + fileManager, + harness: { suggestFiles: vi.fn(async () => undefined) }, + logError: vi.fn(), + } as unknown as HandlerContext; +} + +function createBridge(): BridgeHandler { + const bridge = new BridgeHandler( + vi.fn(), + { get: vi.fn(), update: vi.fn() } as unknown as vscode.Memento, + join(root, "global-storage"), + vi.fn(), + vi.fn(), + vi.fn(), + ); + bridges.push(bridge); + return bridge; +} + +function emptySelection(): vscode.Selection { + return { + isEmpty: true, + start: { line: 0 }, + end: { line: 0 }, + } as vscode.Selection; +} diff --git a/apps/vscode/tsconfig.json b/apps/vscode/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..0a239b4eae2ffb50bc9101e9b9164a4e1a835aec --- /dev/null +++ b/apps/vscode/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["ES2024"], + "types": ["node", "vscode"], + "sourceMap": true, + "declaration": false, + "noEmit": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "paths": { + "@/*": ["./src/*"], + "shared/*": ["./shared/*"], + "@moonshot-ai/kimi-code-sdk": ["../../packages/node-sdk/src/index.ts"] + } + }, + "include": ["src/**/*", "shared/**/*", "test/**/*"], + "exclude": ["dist", "node_modules", "webview-ui", "test/settings-store.test.ts", "test/app-init.test.ts", "test/webview"] +} diff --git a/apps/vscode/tsdown.config.ts b/apps/vscode/tsdown.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1ab8b00991a276c644d7ab370eee6bbfe652273 --- /dev/null +++ b/apps/vscode/tsdown.config.ts @@ -0,0 +1,47 @@ +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; + +import { defineConfig } from 'tsdown'; + +import { rawTextPlugin } from '../../build/raw-text-plugin.mjs'; + +const require = createRequire(import.meta.url); +const pkg = require('./package.json') as { version: string }; +const root = import.meta.dirname; + +export default defineConfig({ + entry: ['./src/extension.ts'], + format: ['esm'], + target: 'node20', + outDir: 'dist', + clean: true, + dts: false, + sourcemap: false, + plugins: [rawTextPlugin()], + alias: { + '@moonshot-ai/kimi-code-sdk': resolve(root, '../../packages/node-sdk/src/index.ts'), + '@moonshot-ai/migration-legacy': resolve(root, '../../packages/migration-legacy/src/index.ts'), + '@moonshot-ai/kaos': resolve(root, '../../packages/kaos/src/index.ts'), + '@moonshot-ai/kimi-code-oauth': resolve(root, '../../packages/oauth/src/index.ts'), + '@moonshot-ai/kosong': resolve(root, '../../packages/kosong/src/index.ts'), + }, + define: { + __EXTENSION_VERSION__: JSON.stringify(pkg.version), + }, + banner: { + js: [ + "import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';", + "import { dirname as __cjsShimDirname } from 'node:path';", + 'const __filename = __cjsShimFileURLToPath(import.meta.url);', + 'const __dirname = __cjsShimDirname(__filename);', + ].join('\n'), + }, + deps: { + onlyBundle: false, + alwaysBundle: [/^@moonshot-ai\//, 'immer', 'zod'], + neverBundle: ['vscode'], + }, + outputOptions: { + entryFileNames: 'extension.js', + }, +}); diff --git a/apps/vscode/vitest.config.ts b/apps/vscode/vitest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..febda27f0db435dbabe8bbf01c9befbc4e7eb641 --- /dev/null +++ b/apps/vscode/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; +import { vscodeProjects } from './vitest.projects'; + +export default defineConfig({ + test: { + projects: vscodeProjects, + }, +}); diff --git a/apps/vscode/vitest.projects.ts b/apps/vscode/vitest.projects.ts new file mode 100644 index 0000000000000000000000000000000000000000..84ee9603d8ba964c0033d9528a0e3859d1009fd6 --- /dev/null +++ b/apps/vscode/vitest.projects.ts @@ -0,0 +1,34 @@ +import react from '@vitejs/plugin-react'; +import { resolve } from 'node:path'; + +const appRoot = import.meta.dirname; + +const alias = { + '@': resolve(appRoot, 'webview-ui/src'), + shared: resolve(appRoot, 'shared'), +}; + +export const vscodeProjects = [ + { + root: appRoot, + resolve: { alias }, + test: { + name: 'extension', + include: ['test/**/*.test.ts'], + exclude: ['test/webview/**'], + environment: 'node', + testTimeout: 15_000, + }, + }, + { + root: appRoot, + plugins: [react()], + resolve: { alias }, + test: { + name: 'webview', + include: ['test/webview/**/*.test.{ts,tsx}'], + environment: 'jsdom', + setupFiles: ['./test/webview/setup.ts'], + }, + }, +]; diff --git a/apps/vscode/webview-ui/components.json b/apps/vscode/webview-ui/components.json new file mode 100644 index 0000000000000000000000000000000000000000..9823830a355d3ad29e6a4876cbd10f69c08b5d8f --- /dev/null +++ b/apps/vscode/webview-ui/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "tabler", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/apps/vscode/webview-ui/index.html b/apps/vscode/webview-ui/index.html new file mode 100644 index 0000000000000000000000000000000000000000..ed8edcd58fd3108f05c307966fceb7dcc9ade690 --- /dev/null +++ b/apps/vscode/webview-ui/index.html @@ -0,0 +1,12 @@ + + + + + + Kimi Code + + +
+ + + diff --git a/apps/vscode/webview-ui/tsconfig.json b/apps/vscode/webview-ui/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..709f8259235f4237c59393e7d28faa19c28bdc8a --- /dev/null +++ b/apps/vscode/webview-ui/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2024"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "ignoreDeprecations": "6.0", + "paths": { + "@/*": ["./src/*"], + "shared/*": ["../shared/*"] + } + }, + "include": ["src", "../test/settings-store.test.ts", "../test/app-init.test.ts", "../test/webview"] +} diff --git a/apps/vscode/webview-ui/vite.config.ts b/apps/vscode/webview-ui/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4ab0a9e20148ebc69df1bd298348e924a04bcb0 --- /dev/null +++ b/apps/vscode/webview-ui/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"; +import { resolve } from "node:path"; + +const webviewRoot = import.meta.dirname; + +export default defineConfig({ + root: webviewRoot, + plugins: [react(), tailwindcss(), cssInjectedByJsPlugin()], + publicDir: resolve(webviewRoot, "public"), + resolve: { + alias: { + "@": resolve(webviewRoot, "./src"), + shared: resolve(webviewRoot, "../shared"), + }, + }, + define: { + "process.env.NODE_ENV": '"production"', + }, + build: { + outDir: resolve(webviewRoot, "../dist"), + emptyOutDir: false, + lib: { + entry: resolve(webviewRoot, "src/main.tsx"), + name: "KimiWebview", + fileName: () => "webview.js", + formats: ["iife"], + }, + rollupOptions: { + output: { + entryFileNames: "webview.js", + assetFileNames: "webview.css", + }, + }, + }, +}); diff --git a/build/raw-text-loader.mjs b/build/raw-text-loader.mjs new file mode 100644 index 0000000000000000000000000000000000000000..708a83b21cd95229f1bce4f485ab05a932083c79 --- /dev/null +++ b/build/raw-text-loader.mjs @@ -0,0 +1,23 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +/** + * Node ESM `load` hook: import `?raw` files as raw-string modules. + * + * This is the runtime counterpart of `build/raw-text-plugin.mjs` (the bundler + * plugin). The plugin covers build (tsdown); this loader covers source + * execution — e.g. `tsx`-run dev flows that import `kimi-core` straight from + * `src`, where no bundler is involved. + */ +export async function load(url, context, nextLoad) { + const [fileUrl, query = ''] = url.split('?', 2); + if (query.split('&').includes('raw')) { + const text = await readFile(fileURLToPath(fileUrl), 'utf-8'); + return { + format: 'module', + shortCircuit: true, + source: `export default ${JSON.stringify(text)};`, + }; + } + return nextLoad(url, context); +} diff --git a/build/raw-text-plugin.mjs b/build/raw-text-plugin.mjs new file mode 100644 index 0000000000000000000000000000000000000000..4bd6a1492a347332f434a1c4bf6ab812ee6595b5 --- /dev/null +++ b/build/raw-text-plugin.mjs @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs'; + +/** + * Bundler plugin that lets files be imported as raw strings: + * + * import description from './grep.md?raw'; + * + * The file content is inlined into the bundle at build time, so prompt + * source files never ship separately in `dist`. Vitest handles the same + * `?raw` imports through Vite's built-in asset loader. + */ +export function rawTextPlugin() { + return { + name: 'raw-text', + enforce: 'pre', + load(id) { + const [path, query = ''] = id.split('?', 2); + if (!query.split('&').includes('raw')) return null; + const text = readFileSync(path, 'utf-8'); + return { code: `export default ${JSON.stringify(text)};`, map: null }; + }, + }; +} diff --git a/build/register-raw-text-loader.mjs b/build/register-raw-text-loader.mjs new file mode 100644 index 0000000000000000000000000000000000000000..c17a3ba6366b98c10cb3488c81c017b61fabd9b9 --- /dev/null +++ b/build/register-raw-text-loader.mjs @@ -0,0 +1,7 @@ +import { register } from 'node:module'; + +/** + * Registers the `?raw` text loader. Pass to Node via `--import` (alongside + * tsx) so source-executed code can import text files. + */ +register('./raw-text-loader.mjs', import.meta.url); diff --git a/scripts/check-nix-workspace.mjs b/scripts/check-nix-workspace.mjs new file mode 100644 index 0000000000000000000000000000000000000000..f27426a1634ee17fe7b4d873c0f58217b1de6345 --- /dev/null +++ b/scripts/check-nix-workspace.mjs @@ -0,0 +1,241 @@ +#!/usr/bin/env node +/** + * Recursively resolve workspace dependencies starting from apps/kimi-code + * and verify they are all present in flake.nix workspaceNames/workspacePaths. + * + * Exit code 0 if everything is in sync, 1 otherwise. + */ + +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { resolve, join } from "node:path"; + +const ROOT = resolve(import.meta.dirname, ".."); +const FLAKE_NIX = join(ROOT, "flake.nix"); +const START_PKG = "@moonshot-ai/kimi-code"; + +/** + * Parse pnpm-workspace.yaml to get workspace directory globs. + */ +function getWorkspaceGlobs() { + const yamlPath = join(ROOT, "pnpm-workspace.yaml"); + const content = readFileSync(yamlPath, "utf8"); + const lines = content.split("\n"); + const globs = []; + let inPackages = false; + for (const line of lines) { + if (line.startsWith("packages:")) { + inPackages = true; + continue; + } + if (inPackages) { + const match = line.match(/^\s+-\s+(.+)$/); + if (match) { + globs.push(match[1]); + } else if (line.trim() !== "" && !line.startsWith(" ")) { + break; + } + } + } + return globs; +} + +/** + * Expand globs like "packages/*" into actual directories. + */ +function expandGlobsSafe(globs) { + const dirs = []; + for (const g of globs) { + if (g.endsWith("/*")) { + const base = g.slice(0, -2); + const basePath = join(ROOT, base); + if (!existsSync(basePath)) continue; + for (const entry of readdirSync(basePath, { withFileTypes: true })) { + if (entry.isDirectory()) { + dirs.push(join(base, entry.name)); + } + } + } else { + const p = join(ROOT, g); + if (existsSync(p)) { + dirs.push(g); + } + } + } + return dirs; +} + +/** + * Build a map of package name -> relative directory for all workspace packages. + */ +function buildWorkspaceMap(dirs) { + const map = new Map(); + for (const dir of dirs) { + const pkgPath = join(ROOT, dir, "package.json"); + if (!existsSync(pkgPath)) continue; + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); + if (pkg.name) { + map.set(pkg.name, dir); + } + } + return map; +} + +/** + * Recursively collect all workspace dependencies (transitive closure). + */ +function resolveWorkspaceDeps(workspaceMap, startName) { + const visited = new Set(); + const closure = new Set(); + + function visit(name) { + if (visited.has(name)) return; + visited.add(name); + + const dir = workspaceMap.get(name); + if (!dir) return; + + const pkgPath = join(ROOT, dir, "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); + const depSections = [ + pkg.dependencies, + pkg.devDependencies, + pkg.peerDependencies, + ]; + + for (const section of depSections) { + if (!section) continue; + for (const [depName, specifier] of Object.entries(section)) { + if ( + typeof specifier === "string" && + (specifier.includes("workspace") || specifier.startsWith("link:")) + ) { + closure.add(depName); + visit(depName); + } + } + } + } + + visit(startName); + return closure; +} + +/** + * Parse workspaceNames and workspacePaths from flake.nix. + */ +function parseFlakeNix() { + const content = readFileSync(FLAKE_NIX, "utf8"); + + function extractArray(label) { + const regex = new RegExp( + `${label}\\s*=\\s*\\[(.*?)\\]`, + "s" + ); + const match = content.match(regex); + if (!match) { + throw new Error(`Could not find ${label} in flake.nix`); + } + const items = []; + // workspaceNames uses quoted strings, workspacePaths uses bare Nix paths + const itemRegex = label === "workspacePaths" ? /\.\/[^\s\]]+/g : /"([^"]+)"/g; + let m; + if (label === "workspacePaths") { + while ((m = itemRegex.exec(match[1])) !== null) { + items.push(m[0]); + } + } else { + while ((m = itemRegex.exec(match[1])) !== null) { + items.push(m[1]); + } + } + return items; + } + + return { + names: extractArray("workspaceNames"), + paths: extractArray("workspacePaths"), + }; +} + +function main() { + const globs = getWorkspaceGlobs(); + const dirs = expandGlobsSafe(globs); + const workspaceMap = buildWorkspaceMap(dirs); + + if (!workspaceMap.has(START_PKG)) { + console.error(`Start package ${START_PKG} not found in workspace.`); + process.exit(1); + } + + const closure = resolveWorkspaceDeps(workspaceMap, START_PKG); + /** @type {string[]} */ + const closureNames = [...closure].sort((a, b) => a.localeCompare(b)); + + const flake = parseFlakeNix(); + const flakeNameSet = new Set(flake.names); + const flakePathSet = new Set(flake.paths); + + const missingNames = closureNames.filter((n) => !flakeNameSet.has(n)); + /** @type {Array<{name: string, path: string}>} */ + const missingPaths = []; + for (const name of closureNames) { + const dir = workspaceMap.get(name); + if (dir && !flakePathSet.has(`./${dir}`)) { + missingPaths.push({ name, path: `./${dir}` }); + } + } + + // Also check that the start package itself is in flake.nix + if (!flakeNameSet.has(START_PKG)) { + missingNames.unshift(START_PKG); + } + const startDir = workspaceMap.get(START_PKG); + if (startDir && !flakePathSet.has(`./${startDir}`)) { + missingPaths.unshift({ name: START_PKG, path: `./${startDir}` }); + } + + const ok = missingNames.length === 0 && missingPaths.length === 0; + + if (!ok) { + console.error("❌ flake.nix workspace lists are out of sync.\n"); + + if (missingNames.length > 0) { + console.error( + "The following workspace packages are missing from flake.nix workspaceNames:" + ); + for (const n of missingNames) { + console.error(` - ${n}`); + } + console.error(""); + } + + if (missingPaths.length > 0) { + console.error( + "The following workspace paths are missing from flake.nix workspacePaths:" + ); + for (const { name, path } of missingPaths) { + console.error(` - ${path} (${name})`); + } + console.error(""); + } + + console.error( + "Please add the missing entries to both workspaceNames and workspacePaths in flake.nix." + ); + console.error( + `\nExpected workspaceNames (${flake.names.length + missingNames.length} total):` + ); + const expectedNames = new Set([...flake.names, ...missingNames.map((m) => m)]); + for (const n of [...expectedNames].sort((a, b) => a.localeCompare(b))) { + console.error(` ${n}`); + } + + process.exit(1); + } + + console.log( + `✅ All ${closureNames.length} recursive workspace dependencies are present in flake.nix.` + ); +} + +main(); diff --git a/scripts/check-no-comments.mjs b/scripts/check-no-comments.mjs new file mode 100644 index 0000000000000000000000000000000000000000..ada91dcd41d3ed287810a28855c6ed1f64bd747d --- /dev/null +++ b/scripts/check-no-comments.mjs @@ -0,0 +1,130 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import ts from 'typescript'; + +const ROOT = path.resolve(import.meta.dirname, '..'); +const PACKAGES = ['packages/agent-core-v2', 'packages/kap-server', 'packages/transcript']; +const DIRS = ['src', 'test', 'scripts']; + +function collectLeaves(node, leaves, jsdocNodes) { + if (ts.isJSDoc(node)) { + jsdocNodes.push(node); + return; + } + const children = node.getChildren(); + if (children.length === 0) { + if (node.getWidth() > 0) leaves.push(node); + return; + } + for (const c of children) collectLeaves(c, leaves, jsdocNodes); +} + +function extractGapComments(gap, offset, out) { + let i = 0; + while (i < gap.length) { + const ch = gap[i]; + if (' \t\n\r\f\v'.includes(ch)) { + i++; + continue; + } + if (ch === '/' && gap[i + 1] === '/') { + let j = gap.indexOf('\n', i); + if (j === -1) j = gap.length; + out.push({ pos: offset + i, end: offset + j, jsdoc: false }); + i = j; + continue; + } + if (ch === '/' && gap[i + 1] === '*') { + const close = gap.indexOf('*/', i + 2); + const j = close === -1 ? gap.length : close + 2; + out.push({ pos: offset + i, end: offset + j, jsdoc: gap.startsWith('/**', i) }); + i = j; + continue; + } + break; + } +} + +function checkFile(file) { + const text = fs.readFileSync(file, 'utf8'); + const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true); + + const leaves = []; + const jsdocNodes = []; + collectLeaves(sf, leaves, jsdocNodes); + leaves.sort((a, b) => a.getStart(sf) - b.getStart(sf)); + + const comments = []; + let cursor = 0; + if (text.startsWith('#!')) { + const nl = text.indexOf('\n'); + cursor = nl === -1 ? text.length : nl + 1; + } + for (const leaf of leaves) { + const s = leaf.getStart(sf); + if (s > cursor) extractGapComments(text.slice(cursor, s), cursor, comments); + cursor = Math.max(cursor, leaf.getEnd()); + } + if (cursor < text.length) extractGapComments(text.slice(cursor), cursor, comments); + for (const d of jsdocNodes) { + comments.push({ pos: d.getStart(sf), end: d.getEnd(), jsdoc: true }); + } + + const seen = new Set(); + const violations = []; + for (const c of comments) { + const key = `${c.pos}:${c.end}`; + if (seen.has(key)) continue; + seen.add(key); + const line = text.slice(0, c.pos).split('\n').length; + const snippet = text.slice(c.pos, Math.min(c.end, c.pos + 60)).replaceAll(/\s+/g, ' '); + if (/(?:oxlint|eslint)-disable/.test(snippet)) continue; + const isDirective = /@ts-(expect-error|ignore|nocheck)|prettier-ignore|istanbul|c8 ignore/.test( + snippet, + ); + violations.push({ line, snippet, isDirective, jsdoc: c.jsdoc }); + } + return violations; +} + +const files = []; +for (const pkg of PACKAGES) { + for (const dir of DIRS) { + const root = path.join(ROOT, pkg, dir); + if (!fs.existsSync(root)) continue; + const stack = [root]; + while (stack.length > 0) { + const d = stack.pop(); + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, e.name); + if (e.isDirectory()) { + if (e.name !== 'node_modules') stack.push(p); + } else if (/\.(ts|tsx|mts|mjs)$/.test(e.name)) { + files.push(p); + } + } + } + } +} + +let total = 0; +for (const f of files) { + const violations = checkFile(f); + for (const v of violations) { + total++; + const rel = path.relative(ROOT, f); + if (v.isDirective) { + console.error(`${rel}:${v.line}: tooling directives are not allowed — fix the underlying lint/type problem instead: ${v.snippet}`); + } else if (v.jsdoc) { + console.error(`${rel}:${v.line}: JSDoc is not allowed in this package: ${v.snippet}`); + } else { + console.error(`${rel}:${v.line}: comments are not allowed in this package: ${v.snippet}`); + } + } +} + +if (total > 0) { + console.error(`check-no-comments: ${total} violation(s) in ${PACKAGES.join(', ')}`); + process.exit(1); +} +console.log(`check-no-comments: OK (${files.length} files)`);