Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/pr-title-checker-config.json +12 -0
- .github/pull_request_template.md +29 -0
- apps/kimi-code/.gitignore +11 -0
- apps/kimi-code/AGENTS.md +72 -0
- apps/kimi-code/CHANGELOG.md +0 -0
- apps/kimi-code/README.md +90 -0
- apps/kimi-code/package.json +120 -0
- apps/kimi-code/tsconfig.dev.json +13 -0
- apps/kimi-code/tsconfig.json +11 -0
- apps/kimi-code/tsdown.config.ts +40 -0
- apps/kimi-code/tsdown.dist-worker.config.ts +41 -0
- apps/kimi-code/tsdown.native.config.ts +69 -0
- apps/kimi-code/tsdown.worker.config.ts +50 -0
- apps/kimi-code/vitest.config.ts +20 -0
- apps/kimi-inspect/AGENTS.md +44 -0
- apps/kimi-inspect/README.md +57 -0
- apps/kimi-inspect/index.html +12 -0
- apps/kimi-inspect/package.json +41 -0
- apps/kimi-inspect/tsconfig.json +22 -0
- apps/kimi-inspect/vite.config.ts +37 -0
- apps/vis/package.json +17 -0
- apps/vscode/.vscode-test.mjs +5 -0
- apps/vscode/.vscodeignore +50 -0
- apps/vscode/CHANGELOG.md +203 -0
- apps/vscode/LICENSE +202 -0
- apps/vscode/README.md +43 -0
- apps/vscode/docs/node-sdk-migration.md +396 -0
- apps/vscode/package.json +321 -0
- apps/vscode/resources/kimi-icon-storefront.png +0 -0
- apps/vscode/resources/kimi-icon.svg +33 -0
- apps/vscode/scripts/extension-host-smoke.d.mts +16 -0
- apps/vscode/scripts/local-cli.mjs +47 -0
- apps/vscode/scripts/ovsx-publish.mjs +51 -0
- apps/vscode/scripts/prepare-dev.mjs +83 -0
- apps/vscode/scripts/publish-args.mjs +54 -0
- apps/vscode/scripts/vsix-package.mjs +141 -0
- apps/vscode/scripts/vsix-publish.mjs +44 -0
- apps/vscode/scripts/vsix-targets.mjs +45 -0
- apps/vscode/scripts/vsix-verify.mjs +490 -0
- apps/vscode/scripts/watch-extension.mjs +96 -0
- apps/vscode/scripts/zip.mjs +122 -0
- apps/vscode/shared/bridge.ts +325 -0
- apps/vscode/shared/errors.ts +106 -0
- apps/vscode/shared/fork-turn-index.ts +43 -0
- apps/vscode/shared/legacy-sdk.ts +233 -0
- apps/vscode/shared/types.ts +73 -0
- apps/vscode/shared/utils.ts +3 -0
- apps/vscode/test/app-init.test.ts +86 -0
- apps/vscode/test/baseline.manager.test.ts +412 -0
- apps/vscode/test/bridge-handler.test.ts +695 -0
.github/pr-title-checker-config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"LABEL": {
|
| 3 |
+
"name": "Invalid PR Title",
|
| 4 |
+
"color": "B60205"
|
| 5 |
+
},
|
| 6 |
+
"CHECKS": {
|
| 7 |
+
"regexp": "^(feat|fix|test|refactor|chore|style|docs|perf|build|ci|revert)(\\(.*\\))?:.*"
|
| 8 |
+
},
|
| 9 |
+
"MESSAGES": {
|
| 10 |
+
"failure": "The PR title is invalid. Please refer to https://www.conventionalcommits.org/en/v1.0.0/ for the convention."
|
| 11 |
+
}
|
| 12 |
+
}
|
.github/pull_request_template.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Thank you for your contribution to Kimi Code!
|
| 3 |
+
External PRs are accepted for approved bug fixes only: link an issue that a maintainer has approved (an `/approve` comment). External feature PRs are not accepted.
|
| 4 |
+
外部 PR 仅接受获批准的 bug 修复:请链接维护者已批准(`/approve` 评论)的 issue;不接受外部 feature PR。
|
| 5 |
+
|
| 6 |
+
See https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md for more.
|
| 7 |
+
-->
|
| 8 |
+
|
| 9 |
+
## Related Issue
|
| 10 |
+
|
| 11 |
+
<!-- Link the issue this change came from. External PRs must link an issue approved by a maintainer (an `/approve` comment) — PRs without one may be closed. -->
|
| 12 |
+
|
| 13 |
+
Resolve #(issue_number)
|
| 14 |
+
|
| 15 |
+
## Problem
|
| 16 |
+
|
| 17 |
+
<!-- What user need or limitation does this address? If the linked issue already covers this, write "See linked issue". -->
|
| 18 |
+
|
| 19 |
+
## What changed
|
| 20 |
+
|
| 21 |
+
<!-- What did you implement, and why does this approach fit Kimi Code? -->
|
| 22 |
+
|
| 23 |
+
## Checklist
|
| 24 |
+
|
| 25 |
+
- [ ] I have read the [CONTRIBUTING](https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md) document.
|
| 26 |
+
- [ ] I have linked a related issue (external PRs: the issue must have a maintainer's `/approve`).
|
| 27 |
+
- [ ] I have added tests that prove my feature works.
|
| 28 |
+
- [ ] Ran `gen-changesets` skill, or this PR needs no changeset.
|
| 29 |
+
- [ ] Ran `gen-docs` skill, or this PR needs no doc update.
|
apps/kimi-code/.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copied from packages/kimi-core at build time
|
| 2 |
+
agents/
|
| 3 |
+
|
| 4 |
+
# Generated at build time by scripts/build-vis-asset.mjs.
|
| 5 |
+
# Only the ~150KB base64 VALUE file is ignored; the committed `.d.ts` stub
|
| 6 |
+
# next to it keeps `#/generated/vis-web-asset` type-resolvable on a fresh
|
| 7 |
+
# clone (before any build has produced the `.ts`).
|
| 8 |
+
src/generated/vis-web-asset.ts
|
| 9 |
+
|
| 10 |
+
# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs
|
| 11 |
+
/native/
|
apps/kimi-code/AGENTS.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# apps/kimi-code Development Guide
|
| 2 |
+
|
| 3 |
+
This file only contains rules local to `apps/kimi-code`. For cross-repo rules, see the root `AGENTS.md`.
|
| 4 |
+
|
| 5 |
+
> **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.
|
| 6 |
+
|
| 7 |
+
## TUI File Layout
|
| 8 |
+
|
| 9 |
+
`apps/kimi-code` is the terminal UI / CLI app. The entry chain is:
|
| 10 |
+
|
| 11 |
+
`src/main.ts` -> `src/cli/commands.ts` -> `src/cli/run-shell.ts` -> SDK `KimiHarness` -> `src/tui/kimi-tui.ts`
|
| 12 |
+
|
| 13 |
+
Main directories:
|
| 14 |
+
|
| 15 |
+
- `src/constant/`: non-copy constants shared by CLI/TUI — product, protocol, paths, terminal control, updates, and so on.
|
| 16 |
+
- `src/cli/`: command-line arguments, subcommands, and CLI startup.
|
| 17 |
+
- `src/tui/`: the interactive terminal UI.
|
| 18 |
+
- `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.
|
| 19 |
+
- `src/tui/tui-state.ts`: `TUIState`, `createTUIState`, `createInitialAppState` — the single global UI-state shape.
|
| 20 |
+
- `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`.
|
| 21 |
+
- `src/tui/commands/`: slash command definitions, parsing, ordering, and dynamic skill command generation.
|
| 22 |
+
- `src/tui/components/`: pi-tui components, organized by UI type.
|
| 23 |
+
- `src/tui/constant/`: non-copy constants reused across TUI modules — symbols, terminal sequences, render sizing, streaming-arg match rules, and so on.
|
| 24 |
+
- `src/tui/components/chrome/`: persistent UI chrome — footer, todo panel, welcome, loader, device code.
|
| 25 |
+
- `src/tui/components/dialogs/`: selectors, approval panels, question popups, and settings popups that temporarily replace the editor.
|
| 26 |
+
- `src/tui/components/editor/`: the custom input box and the file mention provider.
|
| 27 |
+
- `src/tui/components/media/`: image, diff, code highlight, and other media displays.
|
| 28 |
+
- `src/tui/components/messages/`: message blocks in the transcript — assistant, user, tool call, thinking, usage, subagent, and so on.
|
| 29 |
+
- `src/tui/components/panes/`: right-side / activity-area panes such as the activity pane and queue pane.
|
| 30 |
+
- `src/tui/reverse-rpc/`: the adapter layer that bridges SDK approval/question callbacks to the UI.
|
| 31 |
+
- `src/tui/theme/`: themes, color tokens, style helpers, terminal-background detection, and the pi-tui markdown theme.
|
| 32 |
+
- `src/tui/utils/`: TUI-only utility functions.
|
| 33 |
+
- `src/utils/`: app-wide utilities — clipboard, git, history, image, process, usage, and so on.
|
| 34 |
+
|
| 35 |
+
## Module Responsibilities
|
| 36 |
+
|
| 37 |
+
- `cli` only interprets command-line input, assembles startup arguments, and invokes the TUI. Do not put TUI interaction logic into the CLI.
|
| 38 |
+
- `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.
|
| 39 |
+
- `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.
|
| 40 |
+
- `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.
|
| 41 |
+
- `components` only handle presentation and local interaction; they must not call the SDK directly, and must not read or write session state directly.
|
| 42 |
+
- `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.
|
| 43 |
+
- `theme` is the single source of truth for colors and styles. Components must not bypass the theme system and use chalk named colors directly.
|
| 44 |
+
- `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`.
|
| 45 |
+
- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import engine packages directly in app code.
|
| 46 |
+
|
| 47 |
+
## TUI Coding Conventions
|
| 48 |
+
|
| 49 |
+
- Do not over-encapsulate, especially for one- or two-line functions — do not introduce a two-layer wrapper, just inline.
|
| 50 |
+
- Functions with no state / UI side effects do not belong as private methods on the `KimiTUI` class; put them in external utils.
|
| 51 |
+
- Constants must live in the corresponding `constant` directory; they must not be scattered through component or logic code.
|
| 52 |
+
- 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.
|
| 53 |
+
|
| 54 |
+
## Color Rules (normative)
|
| 55 |
+
|
| 56 |
+
The theme apply/switch mechanics live in the `write-tui` skill. The following rules are hard and guard-enforced:
|
| 57 |
+
|
| 58 |
+
- Do not use chalk named colors such as `chalk.red`, `chalk.cyan`, `chalk.white`, `chalk.gray`, `chalk.dim`, or `chalk.yellow` directly.
|
| 59 |
+
- If a component already has `colors`, use `chalk.hex(colors.<token>)(text)`.
|
| 60 |
+
- If a component already has `state.theme.styles` or styles passed in, prefer helpers such as `styles.error(text)`, `styles.dim(text)`.
|
| 61 |
+
- When new visual semantics have no token, first add a semantic field to `ColorPalette`, and fill in both `darkColors` and `lightColors`.
|
| 62 |
+
- 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.
|
| 63 |
+
- 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.
|
| 64 |
+
- 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.
|
| 65 |
+
|
| 66 |
+
## General Coding Requirements
|
| 67 |
+
|
| 68 |
+
- 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.
|
| 69 |
+
- For optional object properties, pass `undefined` directly — do not use conditional spread.
|
| 70 |
+
- Optional object properties do not need to additionally allow `undefined` in the type.
|
| 71 |
+
- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity.
|
| 72 |
+
- Except for a package's own `index.ts`, other `index.ts` files should prefer `export * from './module'`.
|
apps/kimi-code/CHANGELOG.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
apps/kimi-code/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @moonshot-ai/kimi-code
|
| 2 |
+
|
| 3 |
+
> The Starting Point for Next-Gen Agents
|
| 4 |
+
|
| 5 |
+
[](https://www.npmjs.com/package/@moonshot-ai/kimi-code) [](LICENSE) [](https://moonshotai.github.io/kimi-code/en/)
|
| 6 |
+
|
| 7 |
+
## What is Kimi Code CLI
|
| 8 |
+
|
| 9 |
+
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.
|
| 10 |
+
|
| 11 |
+
## Install
|
| 12 |
+
|
| 13 |
+
The recommended install path is the official script. It does not require Node.js to be installed first.
|
| 14 |
+
|
| 15 |
+
- **macOS / Linux**:
|
| 16 |
+
|
| 17 |
+
```sh
|
| 18 |
+
curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
- **Windows (PowerShell)**:
|
| 22 |
+
|
| 23 |
+
```powershell
|
| 24 |
+
irm https://code.kimi.com/kimi-code/install.ps1 | iex
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
> 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`.
|
| 28 |
+
|
| 29 |
+
Then run it with a new Terminal session:
|
| 30 |
+
|
| 31 |
+
```sh
|
| 32 |
+
kimi --version
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Alternative: npm
|
| 36 |
+
|
| 37 |
+
If you prefer npm, use Node.js 22.19.0 or later:
|
| 38 |
+
|
| 39 |
+
```sh
|
| 40 |
+
npm install -g @moonshot-ai/kimi-code
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Or with pnpm:
|
| 44 |
+
|
| 45 |
+
```sh
|
| 46 |
+
pnpm add -g @moonshot-ai/kimi-code
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
For upgrade and uninstall instructions, see the [Getting Started guide](https://moonshotai.github.io/kimi-code/en/guides/getting-started).
|
| 50 |
+
|
| 51 |
+
## Quick Start
|
| 52 |
+
|
| 53 |
+
Open a project and start the interactive UI:
|
| 54 |
+
|
| 55 |
+
```sh
|
| 56 |
+
cd your-project
|
| 57 |
+
kimi
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
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:
|
| 61 |
+
|
| 62 |
+
```
|
| 63 |
+
Take a look at this project and explain the main directories.
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Key Features
|
| 67 |
+
|
| 68 |
+
- **Single-binary distribution.** Install with one command — no Node.js setup, no PATH gymnastics, no global module conflicts.
|
| 69 |
+
- **Blazing-fast startup.** The TUI is ready in milliseconds, so opening a session never feels heavy.
|
| 70 |
+
- **Polished TUI.** A carefully tuned interface designed for long, focused agent sessions.
|
| 71 |
+
- **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.
|
| 72 |
+
- **AI-native MCP configuration.** Add, edit, and authenticate Model Context Protocol servers conversationally via `/mcp-config` — no hand-editing JSON.
|
| 73 |
+
- **Subagents for focused, parallel work.** Dispatch built-in `coder`, `explore`, and `plan` subagents in isolated context windows; the main conversation stays clean.
|
| 74 |
+
- **Lifecycle hooks.** Run local commands at key points — gate risky tool calls, audit decisions, fire desktop notifications, wire into your own automation.
|
| 75 |
+
|
| 76 |
+
## Documentation
|
| 77 |
+
|
| 78 |
+
- Full docs: https://moonshotai.github.io/kimi-code/en/
|
| 79 |
+
- 中文文档: https://moonshotai.github.io/kimi-code/zh/
|
| 80 |
+
- Getting Started: https://moonshotai.github.io/kimi-code/en/guides/getting-started
|
| 81 |
+
|
| 82 |
+
## Repository & Issues
|
| 83 |
+
|
| 84 |
+
- Source: https://github.com/MoonshotAI/kimi-code
|
| 85 |
+
- Issues: https://github.com/MoonshotAI/kimi-code/issues
|
| 86 |
+
- Security: see SECURITY.md in the main repository
|
| 87 |
+
|
| 88 |
+
## License
|
| 89 |
+
|
| 90 |
+
MIT
|
apps/kimi-code/package.json
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@moonshot-ai/kimi-code",
|
| 3 |
+
"version": "0.43.1",
|
| 4 |
+
"description": "The Starting Point for Next-Gen Agents",
|
| 5 |
+
"license": "MIT",
|
| 6 |
+
"author": "Moonshot AI",
|
| 7 |
+
"homepage": "https://github.com/MoonshotAI/kimi-code/tree/main/apps/kimi-code#readme",
|
| 8 |
+
"repository": {
|
| 9 |
+
"type": "git",
|
| 10 |
+
"url": "git+https://github.com/MoonshotAI/kimi-code.git",
|
| 11 |
+
"directory": "apps/kimi-code"
|
| 12 |
+
},
|
| 13 |
+
"bugs": {
|
| 14 |
+
"url": "https://github.com/MoonshotAI/kimi-code/issues"
|
| 15 |
+
},
|
| 16 |
+
"keywords": [
|
| 17 |
+
"kimi",
|
| 18 |
+
"kimi-code",
|
| 19 |
+
"cli",
|
| 20 |
+
"agent",
|
| 21 |
+
"coding-agent",
|
| 22 |
+
"ai",
|
| 23 |
+
"tui"
|
| 24 |
+
],
|
| 25 |
+
"bin": {
|
| 26 |
+
"kimi": "dist/main.mjs"
|
| 27 |
+
},
|
| 28 |
+
"files": [
|
| 29 |
+
"dist",
|
| 30 |
+
"dist-web",
|
| 31 |
+
"native",
|
| 32 |
+
"scripts/postinstall.mjs",
|
| 33 |
+
"scripts/postinstall",
|
| 34 |
+
"README.md"
|
| 35 |
+
],
|
| 36 |
+
"type": "module",
|
| 37 |
+
"imports": {
|
| 38 |
+
"#/tui/theme": "./src/tui/theme/index.ts",
|
| 39 |
+
"#/tui/commands": "./src/tui/commands/index.ts",
|
| 40 |
+
"#/cli/sub/web": "./src/cli/sub/web/index.ts",
|
| 41 |
+
"#/cli/sub/web/*": "./src/cli/sub/web/*.ts",
|
| 42 |
+
"#/generated/vis-web-asset": [
|
| 43 |
+
"./src/generated/vis-web-asset.ts",
|
| 44 |
+
"./src/generated/vis-web-asset.d.ts"
|
| 45 |
+
],
|
| 46 |
+
"#/*": "./src/*.ts"
|
| 47 |
+
},
|
| 48 |
+
"publishConfig": {
|
| 49 |
+
"access": "public",
|
| 50 |
+
"provenance": true
|
| 51 |
+
},
|
| 52 |
+
"scripts": {
|
| 53 |
+
"build": "tsdown && tsdown --config tsdown.dist-worker.config.ts && node scripts/copy-native-assets.mjs && node scripts/check-web-assets.mjs",
|
| 54 |
+
"prebuild": "node scripts/build-vis-asset.mjs",
|
| 55 |
+
"catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json",
|
| 56 |
+
"smoke": "node scripts/smoke.mjs",
|
| 57 |
+
"build:native:js": "node scripts/native/01-bundle.mjs",
|
| 58 |
+
"build:native:sea": "node scripts/native/build.mjs --profile=local",
|
| 59 |
+
"build:native:release": "node scripts/native/build.mjs --profile=release",
|
| 60 |
+
"package:native": "node scripts/native/package.mjs",
|
| 61 |
+
"produce:native:manifest": "node scripts/native/produce-manifest.mjs",
|
| 62 |
+
"release:native:resolve": "node scripts/native/resolve-release.mjs",
|
| 63 |
+
"test:native:smoke": "node scripts/native/smoke.mjs",
|
| 64 |
+
"dev": "node scripts/dev.mjs",
|
| 65 |
+
"dev:cli-only": "tsx --import ../../build/register-raw-text-loader.mjs ./src/main.ts",
|
| 66 |
+
"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",
|
| 67 |
+
"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",
|
| 68 |
+
"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",
|
| 69 |
+
"dev:server:restart": "node scripts/dev-server-restart.mjs",
|
| 70 |
+
"dev:plugin-marketplace": "node scripts/dev-plugin-marketplace-server.mjs",
|
| 71 |
+
"build:plugin-marketplace": "node scripts/build-plugin-marketplace-cdn.mjs",
|
| 72 |
+
"dev:prod": "node dist/main.mjs",
|
| 73 |
+
"clean": "rm -rf dist",
|
| 74 |
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
| 75 |
+
"test": "pnpm -w run build:packages && vitest run",
|
| 76 |
+
"e2e": "pnpm -w run build:packages && KIMI_E2E=1 vitest run test/e2e",
|
| 77 |
+
"e2e:real": "pnpm -w run build:packages && KIMI_E2E_REAL=1 vitest run test/e2e/real-llm-smoke.e2e.test.ts",
|
| 78 |
+
"postinstall": "node scripts/postinstall.mjs"
|
| 79 |
+
},
|
| 80 |
+
"optionalDependencies": {
|
| 81 |
+
"@mariozechner/clipboard": "^0.3.9",
|
| 82 |
+
"node-pty": "^1.1.0"
|
| 83 |
+
},
|
| 84 |
+
"devDependencies": {
|
| 85 |
+
"@moonshot-ai/acp-server": "workspace:^",
|
| 86 |
+
"@moonshot-ai/agent-core-v2": "workspace:^",
|
| 87 |
+
"@moonshot-ai/kap-server": "workspace:^",
|
| 88 |
+
"@moonshot-ai/kimi-code-oauth": "workspace:^",
|
| 89 |
+
"@moonshot-ai/kimi-code-sdk": "workspace:^",
|
| 90 |
+
"@moonshot-ai/kimi-telemetry": "workspace:^",
|
| 91 |
+
"@moonshot-ai/migration-legacy": "workspace:^",
|
| 92 |
+
"@moonshot-ai/minidb": "workspace:^",
|
| 93 |
+
"@moonshot-ai/pi-tui": "workspace:^",
|
| 94 |
+
"@moonshot-ai/remote-control": "workspace:^",
|
| 95 |
+
"@moonshot-ai/vis-server": "workspace:^",
|
| 96 |
+
"@moonshot-ai/vis-web": "workspace:*",
|
| 97 |
+
"@types/qrcode": "^1.5.6",
|
| 98 |
+
"@types/semver": "^7.7.0",
|
| 99 |
+
"@types/ws": "^8.18.0",
|
| 100 |
+
"@types/yazl": "^2.4.6",
|
| 101 |
+
"chalk": "^5.4.1",
|
| 102 |
+
"cli-highlight": "^2.1.11",
|
| 103 |
+
"commander": "^13.1.0",
|
| 104 |
+
"jimp": "^1.6.1",
|
| 105 |
+
"pathe": "^2.0.3",
|
| 106 |
+
"postject": "1.0.0-alpha.6",
|
| 107 |
+
"semver": "^7.7.4",
|
| 108 |
+
"smol-toml": "^1.6.1",
|
| 109 |
+
"tsx": "^4.21.0",
|
| 110 |
+
"yazl": "^3.3.1",
|
| 111 |
+
"zod": "^4.3.6"
|
| 112 |
+
},
|
| 113 |
+
"engines": {
|
| 114 |
+
"node": ">=22.19.0"
|
| 115 |
+
},
|
| 116 |
+
"dependencies": {
|
| 117 |
+
"qrcode": "^1.5.4",
|
| 118 |
+
"ws": "^8.18.0"
|
| 119 |
+
}
|
| 120 |
+
}
|
apps/kimi-code/tsconfig.dev.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "./tsconfig.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"experimentalDecorators": true
|
| 5 |
+
},
|
| 6 |
+
"include": [
|
| 7 |
+
"src",
|
| 8 |
+
"test",
|
| 9 |
+
"../../packages/*/src/**/*.ts",
|
| 10 |
+
"../../packages/*/src/**/*.tsx",
|
| 11 |
+
"../../packages/*/test/**/*.ts"
|
| 12 |
+
]
|
| 13 |
+
}
|
apps/kimi-code/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "../../tsconfig.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"allowJs": true,
|
| 5 |
+
"experimentalDecorators": true,
|
| 6 |
+
"paths": {
|
| 7 |
+
"@/*": ["./src/*"]
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"include": ["src", "test", "../../packages/agent-core-v2/src/env.d.ts"]
|
| 11 |
+
}
|
apps/kimi-code/tsdown.config.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { resolve } from 'node:path';
|
| 2 |
+
|
| 3 |
+
import { defineConfig } from 'tsdown';
|
| 4 |
+
|
| 5 |
+
import { rawTextPlugin } from '../../build/raw-text-plugin.mjs';
|
| 6 |
+
import { BUILT_IN_CATALOG_DEFINE, builtInCatalogDefine } from './scripts/built-in-catalog.mjs';
|
| 7 |
+
|
| 8 |
+
const appRoot = import.meta.dirname;
|
| 9 |
+
|
| 10 |
+
export default defineConfig({
|
| 11 |
+
entry: ['./src/main.ts'],
|
| 12 |
+
format: ['esm'],
|
| 13 |
+
outDir: 'dist',
|
| 14 |
+
clean: true,
|
| 15 |
+
dts: false,
|
| 16 |
+
hash: false,
|
| 17 |
+
banner: {
|
| 18 |
+
js: [
|
| 19 |
+
'#!/usr/bin/env node',
|
| 20 |
+
"import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';",
|
| 21 |
+
"import { dirname as __cjsShimDirname } from 'node:path';",
|
| 22 |
+
'const __filename = __cjsShimFileURLToPath(import.meta.url);',
|
| 23 |
+
'const __dirname = __cjsShimDirname(__filename);',
|
| 24 |
+
].join('\n'),
|
| 25 |
+
},
|
| 26 |
+
plugins: [rawTextPlugin()],
|
| 27 |
+
alias: {
|
| 28 |
+
'@': resolve(appRoot, 'src'),
|
| 29 |
+
},
|
| 30 |
+
define: {
|
| 31 |
+
[BUILT_IN_CATALOG_DEFINE]: builtInCatalogDefine(),
|
| 32 |
+
},
|
| 33 |
+
deps: {
|
| 34 |
+
onlyBundle: false,
|
| 35 |
+
},
|
| 36 |
+
outputOptions: {
|
| 37 |
+
codeSplitting: false,
|
| 38 |
+
entryFileNames: 'main.mjs',
|
| 39 |
+
},
|
| 40 |
+
});
|
apps/kimi-code/tsdown.dist-worker.config.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Bundles the kap-server global-search worker
|
| 2 |
+
// (packages/kap-server/src/search/worker/entry.ts) into ONE self-contained
|
| 3 |
+
// `dist/search-worker.mjs` sibling of the main bundle. The search worker
|
| 4 |
+
// host resolves it at runtime next to `dist/main.mjs` (dev/tests use the TS
|
| 5 |
+
// source; the SEA binary uses the extracted asset from
|
| 6 |
+
// tsdown.worker.config.ts). Separate config because rolldown forbids
|
| 7 |
+
// `codeSplitting: false` with multiple inputs.
|
| 8 |
+
|
| 9 |
+
import { resolve } from 'node:path';
|
| 10 |
+
|
| 11 |
+
import { defineConfig } from 'tsdown';
|
| 12 |
+
|
| 13 |
+
const appRoot = import.meta.dirname;
|
| 14 |
+
|
| 15 |
+
export default defineConfig({
|
| 16 |
+
entry: {
|
| 17 |
+
'search-worker': resolve(
|
| 18 |
+
appRoot,
|
| 19 |
+
'../../packages/kap-server/src/search/worker/entry.ts',
|
| 20 |
+
),
|
| 21 |
+
},
|
| 22 |
+
format: ['esm'],
|
| 23 |
+
// Shares the main bundle's dist (never wipe it) and lands as
|
| 24 |
+
// `dist/search-worker.mjs`.
|
| 25 |
+
outDir: 'dist',
|
| 26 |
+
clean: false,
|
| 27 |
+
dts: false,
|
| 28 |
+
hash: false,
|
| 29 |
+
platform: 'node',
|
| 30 |
+
target: 'node24',
|
| 31 |
+
sourcemap: false,
|
| 32 |
+
minify: false,
|
| 33 |
+
silent: true,
|
| 34 |
+
deps: {
|
| 35 |
+
onlyBundle: false,
|
| 36 |
+
},
|
| 37 |
+
outputOptions: {
|
| 38 |
+
codeSplitting: false,
|
| 39 |
+
entryFileNames: '[name].mjs',
|
| 40 |
+
},
|
| 41 |
+
});
|
apps/kimi-code/tsdown.native.config.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readFileSync } from 'node:fs';
|
| 2 |
+
import { builtinModules } from 'node:module';
|
| 3 |
+
import { resolve } from 'node:path';
|
| 4 |
+
|
| 5 |
+
import { defineConfig } from 'tsdown';
|
| 6 |
+
|
| 7 |
+
import { rawTextPlugin } from '../../build/raw-text-plugin.mjs';
|
| 8 |
+
import { BUILT_IN_CATALOG_DEFINE, builtInCatalogDefine } from './scripts/built-in-catalog.mjs';
|
| 9 |
+
|
| 10 |
+
const appRoot = import.meta.dirname;
|
| 11 |
+
const packageJson = JSON.parse(
|
| 12 |
+
readFileSync(new URL('./package.json', import.meta.url), 'utf-8'),
|
| 13 |
+
) as { version: string };
|
| 14 |
+
|
| 15 |
+
const builtins = new Set([
|
| 16 |
+
...builtinModules,
|
| 17 |
+
...builtinModules.map((name) => `node:${name}`),
|
| 18 |
+
]);
|
| 19 |
+
const optionalNativeDependencies = new Set(['cpu-features']);
|
| 20 |
+
|
| 21 |
+
function shouldAlwaysBundle(id: string): boolean {
|
| 22 |
+
if (builtins.has(id) || id.startsWith('node:')) return false;
|
| 23 |
+
if (optionalNativeDependencies.has(id)) return false;
|
| 24 |
+
// Everything else is force-bundled, which covers `@moonshot-ai/*` (incl.
|
| 25 |
+
// vis-server for `kimi vis`) plus its transitive `hono` / `@hono/node-server`
|
| 26 |
+
// — so the SEA bundle is self-contained (check-bundle.mjs enforces this).
|
| 27 |
+
return true;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function buildTarget(): string {
|
| 31 |
+
return process.env['KIMI_CODE_BUILD_TARGET'] ?? `${process.platform}-${process.arch}`;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export default defineConfig({
|
| 35 |
+
entry: ['./src/main.ts'],
|
| 36 |
+
format: ['cjs'],
|
| 37 |
+
outDir: 'dist-native/intermediates',
|
| 38 |
+
clean: true,
|
| 39 |
+
dts: false,
|
| 40 |
+
fixedExtension: true,
|
| 41 |
+
hash: false,
|
| 42 |
+
platform: 'node',
|
| 43 |
+
target: 'node24',
|
| 44 |
+
banner: { js: '#!/usr/bin/env node' },
|
| 45 |
+
plugins: [rawTextPlugin()],
|
| 46 |
+
alias: {
|
| 47 |
+
'@': resolve(appRoot, 'src'),
|
| 48 |
+
},
|
| 49 |
+
define: {
|
| 50 |
+
[BUILT_IN_CATALOG_DEFINE]: builtInCatalogDefine(),
|
| 51 |
+
__KIMI_CODE_VERSION__: JSON.stringify(packageJson.version),
|
| 52 |
+
__KIMI_CODE_CHANNEL__: JSON.stringify(process.env['KIMI_CODE_CHANNEL'] ?? ''),
|
| 53 |
+
__KIMI_CODE_COMMIT__: JSON.stringify(process.env['KIMI_CODE_COMMIT'] ?? ''),
|
| 54 |
+
__KIMI_CODE_BUILD_TARGET__: JSON.stringify(buildTarget()),
|
| 55 |
+
__KIMI_CODE_NATIVE_BUNDLE__: 'true',
|
| 56 |
+
},
|
| 57 |
+
deps: {
|
| 58 |
+
alwaysBundle: shouldAlwaysBundle,
|
| 59 |
+
neverBundle: [...optionalNativeDependencies],
|
| 60 |
+
onlyBundle: false,
|
| 61 |
+
},
|
| 62 |
+
outputOptions: {
|
| 63 |
+
codeSplitting: false,
|
| 64 |
+
entryFileNames: 'main.cjs',
|
| 65 |
+
},
|
| 66 |
+
checks: {
|
| 67 |
+
legacyCjs: false,
|
| 68 |
+
},
|
| 69 |
+
});
|
apps/kimi-code/tsdown.worker.config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Dedicated tsdown config that bundles the off-main-thread workers into
|
| 2 |
+
// self-contained ESM files so they can ride the SEA blob as assets
|
| 3 |
+
// (02-sea-blob.mjs) and be spawned from disk at runtime:
|
| 4 |
+
// - text-build-worker.mjs: the minidb text-build worker
|
| 5 |
+
// (packages/minidb/src/worker/text-build-worker.ts);
|
| 6 |
+
// - search-worker.mjs: the kap-server global-search worker
|
| 7 |
+
// (packages/kap-server/src/search/worker/entry.ts).
|
| 8 |
+
// Without them the bundled binary lacks the worker entry files on disk and
|
| 9 |
+
// heavy index work degrades to the inline main-thread cores, stalling the
|
| 10 |
+
// event loop on large corpora. Runs after the main bundle with clean:false
|
| 11 |
+
// so all verified files remain.
|
| 12 |
+
//
|
| 13 |
+
// One config per entry: rolldown forbids `codeSplitting: false` with
|
| 14 |
+
// multiple inputs, and each worker must be a single self-contained file
|
| 15 |
+
// (check-bundle.mjs enforces zero remaining externals/relative imports).
|
| 16 |
+
|
| 17 |
+
import { resolve } from 'node:path';
|
| 18 |
+
|
| 19 |
+
import { defineConfig } from 'tsdown';
|
| 20 |
+
|
| 21 |
+
const here = import.meta.dirname;
|
| 22 |
+
|
| 23 |
+
function workerConfig(name: string, entry: string) {
|
| 24 |
+
return defineConfig({
|
| 25 |
+
entry: { [name]: resolve(here, entry) },
|
| 26 |
+
format: ['esm'],
|
| 27 |
+
outDir: resolve(here, 'dist-native/intermediates'),
|
| 28 |
+
entryFileNames: '[name].mjs',
|
| 29 |
+
codeSplitting: false,
|
| 30 |
+
platform: 'node',
|
| 31 |
+
target: 'node24',
|
| 32 |
+
dts: false,
|
| 33 |
+
sourcemap: false,
|
| 34 |
+
minify: false,
|
| 35 |
+
silent: true,
|
| 36 |
+
deps: {
|
| 37 |
+
// Force-bundle the workspace packages the entries import
|
| 38 |
+
// (`@moonshot-ai/minidb` and its subpaths) so the output is
|
| 39 |
+
// self-contained.
|
| 40 |
+
alwaysBundle: [/^@moonshot-ai\//],
|
| 41 |
+
},
|
| 42 |
+
// The intermediates dir also holds main.cjs & co. — never wipe it.
|
| 43 |
+
clean: false,
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export default [
|
| 48 |
+
workerConfig('text-build-worker', '../../packages/minidb/src/worker/text-build-worker.ts'),
|
| 49 |
+
workerConfig('search-worker', '../../packages/kap-server/src/search/worker/entry.ts'),
|
| 50 |
+
];
|
apps/kimi-code/vitest.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { resolve } from 'node:path';
|
| 2 |
+
|
| 3 |
+
import { defineConfig } from 'vitest/config';
|
| 4 |
+
|
| 5 |
+
const appRoot = import.meta.dirname;
|
| 6 |
+
|
| 7 |
+
export default defineConfig({
|
| 8 |
+
resolve: {
|
| 9 |
+
alias: {
|
| 10 |
+
'@': resolve(appRoot, 'src'),
|
| 11 |
+
},
|
| 12 |
+
},
|
| 13 |
+
test: {
|
| 14 |
+
name: 'cli',
|
| 15 |
+
env: {
|
| 16 |
+
KIMI_LOG_LEVEL: 'off',
|
| 17 |
+
},
|
| 18 |
+
include: ['test/**/*.test.ts', 'test/**/*.test.tsx'],
|
| 19 |
+
},
|
| 20 |
+
});
|
apps/kimi-inspect/AGENTS.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# kimi-inspect Agent Guide
|
| 2 |
+
|
| 3 |
+
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.
|
| 4 |
+
|
| 5 |
+
## Top-level views
|
| 6 |
+
|
| 7 |
+
A left icon rail (`src/components/NavRail.tsx`) switches top-level views:
|
| 8 |
+
|
| 9 |
+
- **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.
|
| 10 |
+
- **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).
|
| 11 |
+
- **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.
|
| 12 |
+
- **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`.
|
| 13 |
+
- **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.
|
| 14 |
+
|
| 15 |
+
The **Agent scope** stays in the Chat view's right dock (`src/components/RightPanel.tsx` — Audit / Agent / State / Session tabs) across two of them:
|
| 16 |
+
|
| 17 |
+
- `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.
|
| 18 |
+
- `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`.
|
| 19 |
+
|
| 20 |
+
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()`).
|
| 21 |
+
|
| 22 |
+
## Channel layer
|
| 23 |
+
|
| 24 |
+
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.
|
| 25 |
+
|
| 26 |
+
## Session activity
|
| 27 |
+
|
| 28 |
+
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`).
|
| 29 |
+
|
| 30 |
+
## Dev server
|
| 31 |
+
|
| 32 |
+
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.
|
| 33 |
+
|
| 34 |
+
## Chat view
|
| 35 |
+
|
| 36 |
+
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.
|
| 37 |
+
|
| 38 |
+
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).
|
| 39 |
+
|
| 40 |
+
`/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.
|
| 41 |
+
|
| 42 |
+
## Transcript audit panel
|
| 43 |
+
|
| 44 |
+
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.
|
apps/kimi-inspect/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# kimi-inspect
|
| 2 |
+
|
| 3 |
+
Web inspector for the kap-server `/api/v1/debug` RPC surface — a read/trigger
|
| 4 |
+
window into a running Kimi Code engine (workspaces, sessions, agents, and the
|
| 5 |
+
scoped DI registry).
|
| 6 |
+
|
| 7 |
+
## Run
|
| 8 |
+
|
| 9 |
+
1. Start a kap-server with the debug surface mounted (repo dev scripts do this
|
| 10 |
+
for you): `pnpm dev:v1` / `pnpm dev:v2` from the repo root pass
|
| 11 |
+
`--debug-endpoints` on a loopback bind; the surface inherits the global
|
| 12 |
+
bearer auth.
|
| 13 |
+
2. `pnpm --filter @moonshot-ai/kimi-inspect dev` — the Vite dev server proxies
|
| 14 |
+
`/api` to the server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`)
|
| 15 |
+
and auto-discovers running instances
|
| 16 |
+
(`~/.kimi-code/server/instances`); switch servers from the header dropdown.
|
| 17 |
+
|
| 18 |
+
A connection failure shows a blocking "Debug surface unavailable" screen —
|
| 19 |
+
there is no fallback data source.
|
| 20 |
+
|
| 21 |
+
## Views (left icon rail)
|
| 22 |
+
|
| 23 |
+
- **Chat workspace** — session list (activity badges from the global-events WS)
|
| 24 |
+
plus a transcript-driven per-session chat; the right dock hosts the
|
| 25 |
+
Agent-scope Service panels, a plan lookup card, and the transcript audit
|
| 26 |
+
panel. The Session scope has its own column (pending interactions + session
|
| 27 |
+
Service panels, and a State tab).
|
| 28 |
+
- **Search** — cross-session full-text search over `POST /api/v1/search`
|
| 29 |
+
(cursor-paged; exact-match maps to the API's `literal` mode; a `live`/`index`
|
| 30 |
+
badge shows which server route served the results).
|
| 31 |
+
- **Model Catalog** — every provider with its models and the default marker,
|
| 32 |
+
with per-model ping and session creation actions.
|
| 33 |
+
- **App / Workspace Services** — the full Service reflection over the App
|
| 34 |
+
scope, and over each Workspace scope (picked via the directory browser;
|
| 35 |
+
workspace handlers materialize on demand).
|
| 36 |
+
- **DI** — the engine's Service × Effect × DI debug surface, four panels fed
|
| 37 |
+
by the App-scope debug Services (`IDebugLedgerService` / `IDebugGraphService`
|
| 38 |
+
/ `IDebugCascadeService`) and refreshed eagerly off the `event.di.unit_changed`
|
| 39 |
+
WS frame:
|
| 40 |
+
- **Unit tree** — scope → unit → ledger entries (label, five-state
|
| 41 |
+
`Pending / Activating / Active / Unloading / Failed`, uid, `pinned` flag,
|
| 42 |
+
unit error object), with **unprovide / update / dispose** triggers.
|
| 43 |
+
- **Graph** — the dependency DAG (instance edges across scopes + collection
|
| 44 |
+
edges).
|
| 45 |
+
- **Cascade** — the cascade transaction history ring (changes, contagion set,
|
| 46 |
+
torn-down / rebuilt / failed, abort wait, duration).
|
| 47 |
+
- **Pending** — the waiting area: units parked on unsatisfied dependencies
|
| 48 |
+
with their missing-token sets.
|
| 49 |
+
|
| 50 |
+
## Notes for maintainers
|
| 51 |
+
|
| 52 |
+
- The channel layer (`src/channel/`) is a VS Code-style `ProxyChannel`:
|
| 53 |
+
`GET /api/v1/debug/channels` enumerates every scoped Service — there is no
|
| 54 |
+
whitelist; new Services appear automatically.
|
| 55 |
+
- There is no Service-event push channel besides the global events listed
|
| 56 |
+
above; panels fetch/refresh on demand (react-query, 15 s poll) plus the
|
| 57 |
+
`event.di.unit_changed` invalidation for the DI view.
|
apps/kimi-inspect/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Kimi Inspect</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
apps/kimi-inspect/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@moonshot-ai/kimi-inspect",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"license": "MIT",
|
| 6 |
+
"type": "module",
|
| 7 |
+
"imports": {
|
| 8 |
+
"#/*": {
|
| 9 |
+
"types": [
|
| 10 |
+
"./src/*.ts",
|
| 11 |
+
"./src/*.tsx",
|
| 12 |
+
"./src/*/index.ts",
|
| 13 |
+
"./src/*/index.tsx"
|
| 14 |
+
],
|
| 15 |
+
"default": "./src/*"
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
"scripts": {
|
| 19 |
+
"dev": "vite",
|
| 20 |
+
"build": "vite build",
|
| 21 |
+
"typecheck": "tsc --noEmit",
|
| 22 |
+
"test": "vitest run"
|
| 23 |
+
},
|
| 24 |
+
"dependencies": {
|
| 25 |
+
"@moonshot-ai/agent-core-v2": "workspace:^",
|
| 26 |
+
"@moonshot-ai/kap-server": "workspace:^",
|
| 27 |
+
"@tanstack/react-query": "^5.74.4",
|
| 28 |
+
"react": "^19.1.0",
|
| 29 |
+
"react-dom": "^19.1.0"
|
| 30 |
+
},
|
| 31 |
+
"devDependencies": {
|
| 32 |
+
"@tailwindcss/vite": "^4.1.4",
|
| 33 |
+
"@types/react": "^19.1.2",
|
| 34 |
+
"@types/react-dom": "^19.1.2",
|
| 35 |
+
"@vitejs/plugin-react": "^4.4.1",
|
| 36 |
+
"tailwindcss": "^4.1.4",
|
| 37 |
+
"typescript": "6.0.2",
|
| 38 |
+
"vite": "^6.3.3",
|
| 39 |
+
"vitest": "4.1.4"
|
| 40 |
+
}
|
| 41 |
+
}
|
apps/kimi-inspect/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Mirror the repo root tsconfig: agent-core-v2 is consumed through source
|
| 3 |
+
// `exports`, so its sources are type-checked here too, and the strictness
|
| 4 |
+
// flags must match the root exactly. The src includes are required (not
|
| 5 |
+
// just for speed): agent-core-v2 fills interfaces like `AgentTaskInfoByKind`
|
| 6 |
+
// via cross-module `declare module` augmentation, and those collapse to
|
| 7 |
+
// `never` when the augmenting files are not in the program. Only the
|
| 8 |
+
// environment (DOM + vite client types) differs.
|
| 9 |
+
"extends": "../../tsconfig.json",
|
| 10 |
+
"compilerOptions": {
|
| 11 |
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
| 12 |
+
"types": ["node", "vite/client"],
|
| 13 |
+
"noEmit": true
|
| 14 |
+
},
|
| 15 |
+
"include": [
|
| 16 |
+
"src",
|
| 17 |
+
"vite",
|
| 18 |
+
"vite.config.ts",
|
| 19 |
+
"../../packages/agent-core-v2/src"
|
| 20 |
+
],
|
| 21 |
+
"exclude": ["../../packages/agent-core-v2/src/human"]
|
| 22 |
+
}
|
apps/kimi-inspect/vite.config.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
import tailwindcss from '@tailwindcss/vite';
|
| 4 |
+
|
| 5 |
+
import { serverDiscoveryPlugin } from './vite/serverDiscovery';
|
| 6 |
+
|
| 7 |
+
const webPort = Number(process.env['INSPECT_PORT']) || 5176;
|
| 8 |
+
// Where the dev proxy forwards server traffic. The app can also connect to an
|
| 9 |
+
// arbitrary server URL typed into the connect screen (loopback cross-origin is
|
| 10 |
+
// allowed by kap-server), but the default connection is same-origin through
|
| 11 |
+
// this proxy so no CORS / Origin handling is involved.
|
| 12 |
+
const serverTarget = process.env['KIMI_SERVER_URL'] || 'http://127.0.0.1:58627';
|
| 13 |
+
|
| 14 |
+
export default defineConfig({
|
| 15 |
+
plugins: [react(), tailwindcss(), serverDiscoveryPlugin({ proxyTarget: serverTarget })],
|
| 16 |
+
define: {
|
| 17 |
+
__KIMI_INSPECT_PROXY_TARGET__: JSON.stringify(serverTarget),
|
| 18 |
+
},
|
| 19 |
+
server: {
|
| 20 |
+
port: webPort,
|
| 21 |
+
strictPort: false,
|
| 22 |
+
proxy: {
|
| 23 |
+
'/api': { target: serverTarget, changeOrigin: true, ws: true },
|
| 24 |
+
},
|
| 25 |
+
},
|
| 26 |
+
preview: {
|
| 27 |
+
port: Number(process.env['INSPECT_PREVIEW_PORT']) || 4176,
|
| 28 |
+
proxy: {
|
| 29 |
+
'/api': { target: serverTarget, changeOrigin: true, ws: true },
|
| 30 |
+
},
|
| 31 |
+
},
|
| 32 |
+
build: {
|
| 33 |
+
outDir: 'dist',
|
| 34 |
+
emptyOutDir: true,
|
| 35 |
+
target: 'es2022',
|
| 36 |
+
},
|
| 37 |
+
});
|
apps/vis/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@moonshot-ai/vis",
|
| 3 |
+
"version": "0.1.1",
|
| 4 |
+
"private": true,
|
| 5 |
+
"description": "Debug visualization tool for kimi-code sessions",
|
| 6 |
+
"license": "MIT",
|
| 7 |
+
"type": "module",
|
| 8 |
+
"scripts": {
|
| 9 |
+
"dev": "node scripts/dev.mjs",
|
| 10 |
+
"build": "pnpm --filter @moonshot-ai/vis-server build && pnpm --filter @moonshot-ai/vis-web build && node scripts/copy-web-dist.mjs",
|
| 11 |
+
"prestart": "pnpm run build",
|
| 12 |
+
"start": "node server/dist/server.mjs"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"concurrently": "^9.1.2"
|
| 16 |
+
}
|
| 17 |
+
}
|
apps/vscode/.vscode-test.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "@vscode/test-cli";
|
| 2 |
+
|
| 3 |
+
export default defineConfig({
|
| 4 |
+
files: "out/test/**/*.test.js",
|
| 5 |
+
});
|
apps/vscode/.vscodeignore
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Source files
|
| 2 |
+
src/**
|
| 3 |
+
webview-ui/**
|
| 4 |
+
shared/**
|
| 5 |
+
**/*.ts
|
| 6 |
+
**/*.tsx
|
| 7 |
+
|
| 8 |
+
# Node modules (bundled via esbuild/vite)
|
| 9 |
+
**/node_modules/**
|
| 10 |
+
|
| 11 |
+
# Build configs
|
| 12 |
+
.vscode/**
|
| 13 |
+
tsconfig.json
|
| 14 |
+
tsdown.config.ts
|
| 15 |
+
vitest.config.ts
|
| 16 |
+
.vscode-test.mjs
|
| 17 |
+
eslint.config.mjs
|
| 18 |
+
webview-ui/tsconfig.json
|
| 19 |
+
webview-ui/vite.config.ts
|
| 20 |
+
webview-ui/components.json
|
| 21 |
+
|
| 22 |
+
# Scripts
|
| 23 |
+
scripts/**
|
| 24 |
+
test/**
|
| 25 |
+
runtime/**
|
| 26 |
+
artifacts/**
|
| 27 |
+
|
| 28 |
+
# Never package generated test/debug state
|
| 29 |
+
**/.kimi/**
|
| 30 |
+
**/.kimi-code/**
|
| 31 |
+
**/cache/**
|
| 32 |
+
**/caches/**
|
| 33 |
+
**/credentials/**
|
| 34 |
+
**/logs/**
|
| 35 |
+
**/profile/**
|
| 36 |
+
**/profiles/**
|
| 37 |
+
**/session/**
|
| 38 |
+
**/sessions/**
|
| 39 |
+
**/tokens/**
|
| 40 |
+
**/*.jsonl
|
| 41 |
+
**/*.log
|
| 42 |
+
|
| 43 |
+
# Other
|
| 44 |
+
.gitignore
|
| 45 |
+
**/*.map
|
| 46 |
+
*.vsix
|
| 47 |
+
**/.DS_Store
|
| 48 |
+
|
| 49 |
+
# Prevent parent directory traversal via symlinks
|
| 50 |
+
../**
|
apps/vscode/CHANGELOG.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog
|
| 2 |
+
|
| 3 |
+
## 0.7.5
|
| 4 |
+
|
| 5 |
+
### Patch Changes
|
| 6 |
+
|
| 7 |
+
- [#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.
|
| 8 |
+
|
| 9 |
+
- [#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.
|
| 10 |
+
|
| 11 |
+
- [#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.
|
| 12 |
+
|
| 13 |
+
- Updated dependencies [[`411572e`](https://github.com/MoonshotAI/kimi-code/commit/411572e166edee8581ba9c5a7f1bbf6c8b405606)]:
|
| 14 |
+
- @moonshot-ai/kimi-code-sdk@0.20.0
|
| 15 |
+
|
| 16 |
+
## 0.7.4
|
| 17 |
+
|
| 18 |
+
### Patch Changes
|
| 19 |
+
|
| 20 |
+
- [#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.
|
| 21 |
+
|
| 22 |
+
- [#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.
|
| 23 |
+
|
| 24 |
+
## 0.7.3
|
| 25 |
+
|
| 26 |
+
### Patch Changes
|
| 27 |
+
|
| 28 |
+
- [#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.
|
| 29 |
+
|
| 30 |
+
- [#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.
|
| 31 |
+
|
| 32 |
+
- Updated dependencies [[`75550c5`](https://github.com/MoonshotAI/kimi-code/commit/75550c5686cb867c0cb34bc515997d5e6305fc94), [`d723cc4`](https://github.com/MoonshotAI/kimi-code/commit/d723cc47ee43e5ca3c3c4ec2473f205d44acede2)]:
|
| 33 |
+
- @moonshot-ai/kimi-code-sdk@0.19.2
|
| 34 |
+
|
| 35 |
+
## 0.7.2
|
| 36 |
+
|
| 37 |
+
### Patch Changes
|
| 38 |
+
|
| 39 |
+
- [#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.
|
| 40 |
+
|
| 41 |
+
- Updated dependencies [[`3d77620`](https://github.com/MoonshotAI/kimi-code/commit/3d7762003a4a35cbeb8571d471c6898a006152e6)]:
|
| 42 |
+
- @moonshot-ai/kimi-code-sdk@0.19.1
|
| 43 |
+
|
| 44 |
+
## 0.7.1
|
| 45 |
+
|
| 46 |
+
### Patch Changes
|
| 47 |
+
|
| 48 |
+
- [#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.
|
| 49 |
+
|
| 50 |
+
- 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)]:
|
| 51 |
+
- @moonshot-ai/kimi-code-sdk@0.19.0
|
| 52 |
+
|
| 53 |
+
## 0.7.0
|
| 54 |
+
|
| 55 |
+
### Minor Changes
|
| 56 |
+
|
| 57 |
+
- [#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.
|
| 58 |
+
|
| 59 |
+
### Patch Changes
|
| 60 |
+
|
| 61 |
+
- 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)]:
|
| 62 |
+
- @moonshot-ai/kimi-code-sdk@0.18.0
|
| 63 |
+
|
| 64 |
+
## 0.6.9
|
| 65 |
+
|
| 66 |
+
### Patch Changes
|
| 67 |
+
|
| 68 |
+
- Updated dependencies [[`c9bfe8b`](https://github.com/MoonshotAI/kimi-code/commit/c9bfe8b2c8314ba4ef8806fb3b92ac654c1d1860), [`c212ae9`](https://github.com/MoonshotAI/kimi-code/commit/c212ae9715371c0d7939c15e664acbe0d7cf7fc3)]:
|
| 69 |
+
- @moonshot-ai/kimi-code-sdk@0.17.0
|
| 70 |
+
|
| 71 |
+
## 0.6.8
|
| 72 |
+
|
| 73 |
+
### Patch Changes
|
| 74 |
+
|
| 75 |
+
- 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)]:
|
| 76 |
+
- @moonshot-ai/kimi-code-sdk@0.16.0
|
| 77 |
+
|
| 78 |
+
## 0.6.7
|
| 79 |
+
|
| 80 |
+
### Patch Changes
|
| 81 |
+
|
| 82 |
+
- [#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.
|
| 83 |
+
|
| 84 |
+
## 0.6.6
|
| 85 |
+
|
| 86 |
+
### Patch Changes
|
| 87 |
+
|
| 88 |
+
- [#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.
|
| 89 |
+
- [#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".
|
| 90 |
+
|
| 91 |
+
- Updated dependencies [[`40172c7`](https://github.com/MoonshotAI/kimi-code/commit/40172c7ca96ca981b043b793588dd32e898979fa)]:
|
| 92 |
+
- @moonshot-ai/kimi-code-sdk@0.15.0
|
| 93 |
+
|
| 94 |
+
## 0.6.5
|
| 95 |
+
|
| 96 |
+
### Patch Changes
|
| 97 |
+
|
| 98 |
+
- [#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.
|
| 99 |
+
- 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)]:
|
| 100 |
+
- @moonshot-ai/kimi-code-sdk@0.14.0
|
| 101 |
+
|
| 102 |
+
## 0.6.4
|
| 103 |
+
|
| 104 |
+
### Changed
|
| 105 |
+
|
| 106 |
+
- Picking a model's highest thinking effort now applies to the current session
|
| 107 |
+
only instead of becoming the global default: the top tier saves just the
|
| 108 |
+
on/off toggle, lower tiers persist as the default as before, and
|
| 109 |
+
re-confirming the current effort no longer rewrites the saved preference.
|
| 110 |
+
The model and thinking pickers also note that switching mid-conversation
|
| 111 |
+
invalidates the existing prompt cache.
|
| 112 |
+
- Unified the YOLO and Auto permission mode naming and descriptions with the
|
| 113 |
+
CLI (`/afk` is now `/auto`), and approval requests that fall outside the
|
| 114 |
+
active permission mode (sensitive files, plan reviews, ask rules) are now
|
| 115 |
+
always shown to you instead of being auto-approved.
|
| 116 |
+
|
| 117 |
+
## 0.6.3
|
| 118 |
+
|
| 119 |
+
### Fixed
|
| 120 |
+
|
| 121 |
+
- Editor mentions now work for files outside the working directory, and paths
|
| 122 |
+
containing spaces are quoted correctly.
|
| 123 |
+
- Cancelling a running turn now reliably reaches the engine, and the UI no
|
| 124 |
+
longer reports a task as stopped when there is nothing to cancel.
|
| 125 |
+
- Attaching to or resuming an existing session no longer overwrites its model
|
| 126 |
+
and thinking effort with the configured defaults; model or effort changes
|
| 127 |
+
picked in the composer are applied when the prompt is sent.
|
| 128 |
+
|
| 129 |
+
## 0.6.2
|
| 130 |
+
|
| 131 |
+
### Fixed
|
| 132 |
+
|
| 133 |
+
- A core error arriving in the middle of a turn no longer corrupts the active
|
| 134 |
+
turn; the turn now ends cleanly with an error instead of leaving the chat in
|
| 135 |
+
a broken state.
|
| 136 |
+
- Kimi sign-in and connection failures now include the underlying transport
|
| 137 |
+
cause (for example DNS or connection refused) instead of a generic error.
|
| 138 |
+
- Closed several FetchURL SSRF bypasses and the DNS-rebinding window.
|
| 139 |
+
- Tool calls interrupted mid-stream are now recorded and closed, so they no
|
| 140 |
+
longer corrupt the session history.
|
| 141 |
+
|
| 142 |
+
## 0.6.1
|
| 143 |
+
|
| 144 |
+
### Fixed
|
| 145 |
+
|
| 146 |
+
- The **Sign in** action in the settings (gear) menu now actually starts the
|
| 147 |
+
Kimi login flow and shows an error toast when sign-in fails, instead of
|
| 148 |
+
silently doing nothing.
|
| 149 |
+
|
| 150 |
+
## 0.6.0
|
| 151 |
+
|
| 152 |
+
### Breaking
|
| 153 |
+
|
| 154 |
+
- Raised the minimum supported editor version to VS Code 1.100.0.
|
| 155 |
+
- Legacy Kimi Code OAuth credentials and MCP OAuth credentials are deliberately
|
| 156 |
+
not migrated. Sign in to Kimi Code again and re-authorize affected MCP
|
| 157 |
+
servers after upgrading.
|
| 158 |
+
- Removed the `kimi.executablePath` and `kimi.environmentVariables` settings.
|
| 159 |
+
The old `kimi.environmentVariables.KIMI_SHARE_DIR` value is consulted only to
|
| 160 |
+
discover legacy data during migration; it is not applied to the new runtime.
|
| 161 |
+
The system-level `KIMI_CODE_HOME` environment variable remains supported.
|
| 162 |
+
|
| 163 |
+
### Changed
|
| 164 |
+
|
| 165 |
+
- Replaced the legacy Python/stdio runtime with the in-process Kimi Code Node
|
| 166 |
+
SDK. The extension no longer downloads or starts a separate Kimi executable.
|
| 167 |
+
- The in-process engine is the same one that powers the Kimi Code CLI, so the
|
| 168 |
+
agent gains CLI-parity capabilities beyond the legacy runtime, including
|
| 169 |
+
parallel subagent swarms, background tasks, and long-running goal runs.
|
| 170 |
+
- Added an opt-in legacy migration prompt on the first launch that detects data
|
| 171 |
+
from version 0.5.x. The migration copies or merges supported data into the
|
| 172 |
+
current Kimi Code home and does not delete the legacy source. If migration is
|
| 173 |
+
skipped or needs to be retried, run **Kimi Code: Migrate Legacy Data** from the
|
| 174 |
+
Command Palette.
|
| 175 |
+
- When VS Code and the Kimi Code terminal app resolve to the same
|
| 176 |
+
`KIMI_CODE_HOME`, they use the same configuration and session storage. Running
|
| 177 |
+
the same session concurrently from multiple processes is not supported or
|
| 178 |
+
protected by cross-process locking.
|
| 179 |
+
- The model picker groups models by provider when multiple providers are
|
| 180 |
+
configured, keeps provider identity when display names match, and recognizes
|
| 181 |
+
adaptive-thinking metadata. A configured custom default provider no longer
|
| 182 |
+
requires dismissing the Kimi account login screen on every launch.
|
| 183 |
+
- The file changes panel and Undo actions use extension-maintained baselines.
|
| 184 |
+
Files changed through Kimi's Write and Edit operations are tracked on a
|
| 185 |
+
best-effort basis. File deletions performed inside Bash are not tracked by
|
| 186 |
+
this baseline and therefore cannot be restored by the panel's Undo action.
|
| 187 |
+
|
| 188 |
+
### Fixed
|
| 189 |
+
|
| 190 |
+
- The `kimi.yoloMode` setting now reaches the permission engine: enabling it
|
| 191 |
+
maps to the core `yolo` permission mode and takes effect when a session
|
| 192 |
+
attaches, including sessions that previously stored a disabled auto-approve
|
| 193 |
+
state.
|
| 194 |
+
- Kept the chat header and input toolbar readable when the sidebar is narrow:
|
| 195 |
+
controls wrap and shrink instead of being clipped.
|
| 196 |
+
|
| 197 |
+
### Distribution boundary
|
| 198 |
+
|
| 199 |
+
Release packaging produces target-specific VSIX files for `darwin-x64`,
|
| 200 |
+
`darwin-arm64`, `linux-x64`, `linux-arm64`, `win32-x64`, and `win32-arm64`.
|
| 201 |
+
Archive and static verification for a target does not by itself prove that the
|
| 202 |
+
extension has run successfully in that target's Extension Host; runtime test
|
| 203 |
+
results must be recorded separately for each operating system and architecture.
|
apps/vscode/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
apps/vscode/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Kimi Code
|
| 2 |
+
|
| 3 |
+
AI coding assistant for VS Code, built for long-context workflows and complex coding tasks.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Works alongside you**: Kimi autonomously explores your codebase, reads and writes code, and runs terminal commands with your permission
|
| 8 |
+
- **Thinking controls**: Toggle reasoning or choose a model-supported thinking effort
|
| 9 |
+
- **Provider-aware models**: Distinguish and select same-named models across configured providers
|
| 10 |
+
- **Native editor integration**: Review AI-proposed changes directly in VS Code's diff viewer
|
| 11 |
+
- **MCP support**: Extend capabilities with Model Context Protocol servers
|
| 12 |
+
- **Slash commands**: Quick actions like `/init` to analyze your project and `/compact` to manage context
|
| 13 |
+
|
| 14 |
+
## Install
|
| 15 |
+
|
| 16 |
+
Kimi Code requires VS Code 1.100.0 or later.
|
| 17 |
+
|
| 18 |
+
1. Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=moonshot-ai.kimi-code)
|
| 19 |
+
2. Open a folder in VS Code
|
| 20 |
+
3. Click the Kimi icon in the Activity Bar
|
| 21 |
+
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`
|
| 22 |
+
|
| 23 |
+
The extension runs the Kimi Code Node SDK in the VS Code Extension Host. When
|
| 24 |
+
the extension and the Kimi Code terminal app resolve to the same
|
| 25 |
+
`KIMI_CODE_HOME`, they share `config.toml`, MCP configuration, login state, and
|
| 26 |
+
sessions. The system-level `KIMI_CODE_HOME` environment variable is supported;
|
| 27 |
+
there is no separate VS Code setting for it. Do not run the same session from
|
| 28 |
+
both applications at the same time, because cross-process session locking is
|
| 29 |
+
not guaranteed.
|
| 30 |
+
|
| 31 |
+
After upgrading from version 0.5.x, the extension prompts before migrating any
|
| 32 |
+
legacy data it finds. Migration copies or merges data into the current Kimi Code
|
| 33 |
+
home and does not delete the legacy source. Legacy Kimi Code OAuth and MCP OAuth
|
| 34 |
+
credentials are not copied, so those connections must be authorized again.
|
| 35 |
+
See [the changelog](CHANGELOG.md) for the full compatibility notes.
|
| 36 |
+
|
| 37 |
+
## Docs
|
| 38 |
+
|
| 39 |
+
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)
|
| 40 |
+
|
| 41 |
+
## License
|
| 42 |
+
|
| 43 |
+
[Apache-2.0](LICENSE)
|
apps/vscode/docs/node-sdk-migration.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# VS Code Node SDK Migration Design
|
| 2 |
+
|
| 3 |
+
Status: accepted and implemented for extension `0.6.0`
|
| 4 |
+
|
| 5 |
+
Last updated: 2026-07-16
|
| 6 |
+
|
| 7 |
+
## Context
|
| 8 |
+
|
| 9 |
+
The `0.5.x` VS Code extension launched a separately installed Python Kimi CLI
|
| 10 |
+
and communicated with it over stdio. That architecture duplicated runtime
|
| 11 |
+
installation, configuration, authentication, and session behavior between the
|
| 12 |
+
editor and Kimi Code.
|
| 13 |
+
|
| 14 |
+
Version `0.6.0` moves the extension into this monorepo under `apps/vscode` and
|
| 15 |
+
runs the stable TypeScript v1 engine through `@moonshot-ai/kimi-code-sdk` in the
|
| 16 |
+
VS Code Extension Host. The migration preserves the existing extension ID,
|
| 17 |
+
commands, Webview, and user-visible workflows. It does not redesign the UI or
|
| 18 |
+
introduce unrelated TUI features.
|
| 19 |
+
|
| 20 |
+
This document records the durable design decisions behind that migration. It
|
| 21 |
+
is not a release checklist or a transcript of the implementation process.
|
| 22 |
+
|
| 23 |
+
## Goals
|
| 24 |
+
|
| 25 |
+
- Keep the extension ID `moonshot-ai.kimi-code` so `0.6.0` upgrades existing
|
| 26 |
+
installations.
|
| 27 |
+
- Preserve the existing VS Code commands, shortcuts, Webview workflows, editor
|
| 28 |
+
integration, session management, MCP management, and file changes panel.
|
| 29 |
+
- Replace the Python/stdio host with the in-process v1 Node SDK.
|
| 30 |
+
- Share Kimi Code configuration, authentication, MCP configuration, and
|
| 31 |
+
sessions with the TUI when both processes resolve the same Kimi Code home.
|
| 32 |
+
- Reuse the shared legacy migration package instead of maintaining a VS
|
| 33 |
+
Code-specific session translator.
|
| 34 |
+
- Add only the smallest SDK/core APIs needed to preserve existing VS Code
|
| 35 |
+
behavior.
|
| 36 |
+
- Package and test platform-targeted VSIX artifacts for macOS, Linux, and
|
| 37 |
+
Windows.
|
| 38 |
+
|
| 39 |
+
The only intentional UI capability added during the migration is
|
| 40 |
+
model-aware thinking effort selection. It is necessary to represent the model
|
| 41 |
+
capabilities exposed by the v1 configuration.
|
| 42 |
+
|
| 43 |
+
## Non-goals
|
| 44 |
+
|
| 45 |
+
- Replacing the React Webview with VS Code native views or Chat Participants.
|
| 46 |
+
- Migrating to the v2 engine.
|
| 47 |
+
- Adding TUI-only features such as goals, cron, swarm, or BTW.
|
| 48 |
+
- Keeping a Python CLI fallback or a custom executable setting.
|
| 49 |
+
- Adding cross-process locks for concurrent access to one session.
|
| 50 |
+
- Making the core wait for VS Code before file tools execute.
|
| 51 |
+
- Parsing arbitrary shell commands to infer file changes.
|
| 52 |
+
- Copying legacy OAuth or MCP OAuth credentials.
|
| 53 |
+
- Publishing an extension as part of the build or package commands.
|
| 54 |
+
|
| 55 |
+
## Runtime architecture
|
| 56 |
+
|
| 57 |
+
```mermaid
|
| 58 |
+
flowchart LR
|
| 59 |
+
UI["React Webview<br/>browser sandbox"]
|
| 60 |
+
Host["VS Code Extension Host<br/>Node process"]
|
| 61 |
+
SDK["@moonshot-ai/kimi-code-sdk<br/>KimiHarness and Session"]
|
| 62 |
+
Core["agent-core-v2"]
|
| 63 |
+
Home["Kimi Code home<br/>config, auth, MCP, sessions"]
|
| 64 |
+
|
| 65 |
+
UI <-->|"postMessage RPC and events"| Host
|
| 66 |
+
Host -->|"in-process calls"| SDK
|
| 67 |
+
SDK -->|"in-process calls"| Core
|
| 68 |
+
SDK <--> Home
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
There is no Python process, secondary Node process, or local HTTP server in the
|
| 72 |
+
production extension path.
|
| 73 |
+
|
| 74 |
+
The Webview remains because it is the existing product UI. It cannot import the
|
| 75 |
+
Node SDK directly: the Webview is a browser sandbox and must not access the file
|
| 76 |
+
system, credentials, process environment, or session storage. Those operations
|
| 77 |
+
stay in the trusted Extension Host.
|
| 78 |
+
|
| 79 |
+
### Extension identity
|
| 80 |
+
|
| 81 |
+
The runtime constructs the SDK client with:
|
| 82 |
+
|
| 83 |
+
- `productName: "kimi-code-vscode"`
|
| 84 |
+
- `version` from `apps/vscode/package.json`
|
| 85 |
+
- `uiMode: "vscode"`
|
| 86 |
+
|
| 87 |
+
For `0.6.0`, the normal HTTP User-Agent product is therefore
|
| 88 |
+
`kimi-code-vscode/0.6.0`. The version has one source of truth and is not copied
|
| 89 |
+
into runtime code or packaging scripts.
|
| 90 |
+
|
| 91 |
+
### Package boundaries
|
| 92 |
+
|
| 93 |
+
- `apps/vscode` depends on `@moonshot-ai/kimi-code-sdk`.
|
| 94 |
+
- `apps/vscode` must not depend directly on engine packages.
|
| 95 |
+
- Core capabilities needed by released clients are exposed through the Node SDK
|
| 96 |
+
and tested at that public boundary.
|
| 97 |
+
- The Webview communicates only through the typed bridge in
|
| 98 |
+
`apps/vscode/shared`.
|
| 99 |
+
|
| 100 |
+
## Main components
|
| 101 |
+
|
| 102 |
+
| Area | Primary implementation |
|
| 103 |
+
|---|---|
|
| 104 |
+
| Activation and VS Code commands | `apps/vscode/src/extension.ts` |
|
| 105 |
+
| Webview lifecycle | `apps/vscode/src/KimiWebviewProvider.ts` |
|
| 106 |
+
| Webview RPC boundary | `apps/vscode/src/bridge-handler.ts`, `apps/vscode/src/handlers` |
|
| 107 |
+
| SDK host | `apps/vscode/src/runtime/kimi-runtime.ts` |
|
| 108 |
+
| Session lifecycle and event routing | `apps/vscode/src/runtime/session-runtime.ts` |
|
| 109 |
+
| SDK-to-Webview event conversion | `apps/vscode/src/runtime/event-adapter.ts` |
|
| 110 |
+
| Session replay | `apps/vscode/src/runtime/replay-adapter.ts` |
|
| 111 |
+
| File changes and baselines | `apps/vscode/src/managers` |
|
| 112 |
+
| Legacy migration coordination | `apps/vscode/src/migration` |
|
| 113 |
+
| React UI | `apps/vscode/webview-ui` |
|
| 114 |
+
| Packaging and smoke tests | `apps/vscode/scripts`, `.github/workflows/ci.yml` |
|
| 115 |
+
|
| 116 |
+
## Data ownership
|
| 117 |
+
|
| 118 |
+
### Shared Kimi Code home
|
| 119 |
+
|
| 120 |
+
The SDK resolves the home directory using the normal Kimi Code rules:
|
| 121 |
+
|
| 122 |
+
1. system-level `KIMI_CODE_HOME`, when set;
|
| 123 |
+
2. otherwise `~/.kimi-code`.
|
| 124 |
+
|
| 125 |
+
The extension does not add a separate `kimi.homeDir` setting and does not pass
|
| 126 |
+
its own default home to the SDK. VS Code and the TUI share the following data
|
| 127 |
+
only when they resolve the same home:
|
| 128 |
+
|
| 129 |
+
- `config.toml`
|
| 130 |
+
- `mcp.json`
|
| 131 |
+
- authentication state
|
| 132 |
+
- `sessions/`
|
| 133 |
+
- `session_index.jsonl`
|
| 134 |
+
- other SDK-owned Kimi Code data
|
| 135 |
+
|
| 136 |
+
Remote SSH, WSL, and Dev Container installations use the environment and home
|
| 137 |
+
of the remote Extension Host. They do not automatically share the local
|
| 138 |
+
machine's Kimi Code home.
|
| 139 |
+
|
| 140 |
+
### VS Code-owned state
|
| 141 |
+
|
| 142 |
+
VS Code settings and Extension Host storage own editor-specific behavior:
|
| 143 |
+
|
| 144 |
+
- autosave and keyboard behavior;
|
| 145 |
+
- thinking display preferences;
|
| 146 |
+
- editor context injection mode;
|
| 147 |
+
- Webview state;
|
| 148 |
+
- file change baselines and legacy baseline acceptance markers.
|
| 149 |
+
|
| 150 |
+
These values are not written into the shared core configuration unless they
|
| 151 |
+
are already a shared product setting, such as the selected model or thinking
|
| 152 |
+
effort.
|
| 153 |
+
|
| 154 |
+
### Environment variables
|
| 155 |
+
|
| 156 |
+
The old `kimi.environmentVariables` setting existed to populate the environment
|
| 157 |
+
of the Python child process. It was removed with that process model.
|
| 158 |
+
|
| 159 |
+
- Provider-specific environment variables remain in `config.toml`.
|
| 160 |
+
- MCP server environment variables remain in `mcp.json`.
|
| 161 |
+
- proxy and other process-level variables are inherited from the Extension
|
| 162 |
+
Host environment.
|
| 163 |
+
- a legacy `KIMI_SHARE_DIR` in the removed setting is consulted only as an
|
| 164 |
+
additional migration source.
|
| 165 |
+
- other values from the removed global environment map are not migrated.
|
| 166 |
+
|
| 167 |
+
## Webview bridge
|
| 168 |
+
|
| 169 |
+
The bridge keeps the request/response and event-broadcast shape of the previous
|
| 170 |
+
extension so the React UI did not require a product redesign. The Extension
|
| 171 |
+
Host validates method names, payloads, workspace containment, and file paths.
|
| 172 |
+
The Webview uses a nonce-based content security policy and does not receive
|
| 173 |
+
tokens, complete configuration files, or unnecessary absolute paths.
|
| 174 |
+
|
| 175 |
+
The event adapter maps v1 SDK events such as `turn.started`,
|
| 176 |
+
`assistant.delta`, `tool.call.started`, approvals, questions, compaction, and
|
| 177 |
+
errors into the UI state expected by the migrated Webview. Session replay uses
|
| 178 |
+
the SDK/core replay surface rather than parsing current core storage directly
|
| 179 |
+
from the UI.
|
| 180 |
+
|
| 181 |
+
Provider errors may arrive after a failed turn-end event. Session subscriptions
|
| 182 |
+
therefore remain alive long enough to deliver the final error instead of being
|
| 183 |
+
cancelled immediately when a turn ends.
|
| 184 |
+
|
| 185 |
+
## Sessions
|
| 186 |
+
|
| 187 |
+
The Node SDK exposes the session operations required by the existing editor UI:
|
| 188 |
+
|
| 189 |
+
- create, resume, list, rename, and export;
|
| 190 |
+
- permanent delete;
|
| 191 |
+
- fork from a selected historical turn;
|
| 192 |
+
- replayed context and records needed to restore the UI;
|
| 193 |
+
- approval, question, stop, steer, plan mode, model, and thinking controls.
|
| 194 |
+
|
| 195 |
+
Permanent delete keeps the old VS Code meaning; it is not silently mapped to
|
| 196 |
+
archive. A running target session is stopped and closed before its persisted
|
| 197 |
+
data and index references are removed.
|
| 198 |
+
|
| 199 |
+
Historical fork keeps the selected turn and its preceding context, then removes
|
| 200 |
+
later conversation state. Baselines are materialized into the fork so Keep or
|
| 201 |
+
Undo actions in one session cannot affect the other.
|
| 202 |
+
|
| 203 |
+
The TUI and VS Code may resume sessions created by each other. They must not run
|
| 204 |
+
the same session concurrently because the v1 store has no cross-process write
|
| 205 |
+
lock.
|
| 206 |
+
|
| 207 |
+
## Provider-aware models and thinking
|
| 208 |
+
|
| 209 |
+
Model aliases retain their provider identity. The picker groups models by
|
| 210 |
+
provider when multiple providers are configured, so same-named models remain
|
| 211 |
+
distinct. Media fallback prefers a compatible model from the current provider
|
| 212 |
+
before considering another provider.
|
| 213 |
+
|
| 214 |
+
Thinking controls follow each model's declared capabilities:
|
| 215 |
+
|
| 216 |
+
- models with `support_efforts` expose those effort values;
|
| 217 |
+
- boolean-thinking models expose on/off;
|
| 218 |
+
- `always_thinking` models do not expose an invalid off state;
|
| 219 |
+
- adaptive-thinking capability is preserved when applying the selection.
|
| 220 |
+
|
| 221 |
+
Changing the model or thinking effort updates the active session and the shared
|
| 222 |
+
default configuration used by new TUI and VS Code sessions.
|
| 223 |
+
|
| 224 |
+
## MCP
|
| 225 |
+
|
| 226 |
+
Home-level MCP operations are exposed by the SDK harness rather than by direct
|
| 227 |
+
file access from the extension. They cover configuration CRUD, OAuth/reset,
|
| 228 |
+
connection testing, and user-global `mcp.json` entries. Session-level status and
|
| 229 |
+
reconnect behavior remain on the Session surface.
|
| 230 |
+
|
| 231 |
+
The extension supports stdio and HTTP servers, including their environment,
|
| 232 |
+
headers, and authorization flows. Legacy MCP OAuth credentials are not copied
|
| 233 |
+
and may require authorization after upgrade.
|
| 234 |
+
|
| 235 |
+
## Legacy migration
|
| 236 |
+
|
| 237 |
+
Migration is opt-in and uses `@moonshot-ai/migration-legacy` for detection and
|
| 238 |
+
translation. The extension coordinates prompts and reports but does not
|
| 239 |
+
maintain another config/session translator.
|
| 240 |
+
|
| 241 |
+
### Sources and target
|
| 242 |
+
|
| 243 |
+
- default source: `~/.kimi`;
|
| 244 |
+
- optional additional source: a valid legacy `KIMI_SHARE_DIR` from the removed
|
| 245 |
+
VS Code setting;
|
| 246 |
+
- target: the SDK-resolved Kimi Code home.
|
| 247 |
+
|
| 248 |
+
Migration covers the shared config, MCP config, user history, supported skills,
|
| 249 |
+
and sessions. Existing target data wins according to the shared migration
|
| 250 |
+
package's conflict rules. Migration is repeatable and does not delete the
|
| 251 |
+
legacy source.
|
| 252 |
+
|
| 253 |
+
On first launch, the extension detects work without mutating either home and
|
| 254 |
+
offers **Migrate now** or **Later**. The command
|
| 255 |
+
`Kimi Code: Migrate Legacy Data` remains available for manual runs and retries.
|
| 256 |
+
|
| 257 |
+
The shared marker `.migrated-to-kimi-code` can contain multiple target homes.
|
| 258 |
+
This prevents duplicate migration when the TUI migrated the same source first,
|
| 259 |
+
while still allowing a different `KIMI_CODE_HOME` to be migrated later.
|
| 260 |
+
|
| 261 |
+
Migrated sessions keep source metadata in `state.json.custom`, including the
|
| 262 |
+
legacy source path and session identity. This metadata also supports legacy
|
| 263 |
+
baseline fallback.
|
| 264 |
+
|
| 265 |
+
OAuth and MCP OAuth credentials are intentionally not copied. Refresh tokens
|
| 266 |
+
may rotate, so copying them can invalidate one installation or create ambiguous
|
| 267 |
+
ownership. The upgrade flow and release notes must tell users to authorize
|
| 268 |
+
again when needed.
|
| 269 |
+
|
| 270 |
+
## File changes and baselines
|
| 271 |
+
|
| 272 |
+
The baseline feature exists only to support the VS Code File Changes panel,
|
| 273 |
+
diff view, Keep Changes, and Undo. It is not core session state and is not a TUI
|
| 274 |
+
feature.
|
| 275 |
+
|
| 276 |
+
### New sessions
|
| 277 |
+
|
| 278 |
+
Baselines are stored under the extension's `globalStorage`, namespaced by the
|
| 279 |
+
resolved Kimi Code home and session ID. The home namespace prevents sessions
|
| 280 |
+
with the same ID in different homes from sharing baseline state.
|
| 281 |
+
|
| 282 |
+
The session runtime observes `tool.call.started` for the explicit `Write` and
|
| 283 |
+
`Edit` tools. It captures the first pre-change content for each file and
|
| 284 |
+
refreshes the File Changes panel after tool results. A missing original file is
|
| 285 |
+
represented as a newly created file.
|
| 286 |
+
|
| 287 |
+
- **Keep** removes the effective baseline entry from the panel.
|
| 288 |
+
- **Undo** restores the original content or deletes a file that did not exist.
|
| 289 |
+
- keeping a file and editing it again starts a new baseline period.
|
| 290 |
+
- all paths are resolved against the session work directory and checked for
|
| 291 |
+
traversal and symlink escape.
|
| 292 |
+
|
| 293 |
+
This is intentionally best-effort. The core does not pause tool execution for a
|
| 294 |
+
VS Code callback, and arbitrary Bash file mutations are not tracked.
|
| 295 |
+
|
| 296 |
+
### Migrated sessions
|
| 297 |
+
|
| 298 |
+
Legacy baselines are not copied in bulk. A migrated session uses its recorded
|
| 299 |
+
legacy source path and reads `<legacy-session>/baseline` as a read-only
|
| 300 |
+
fallback.
|
| 301 |
+
|
| 302 |
+
The lookup order is:
|
| 303 |
+
|
| 304 |
+
1. extension-owned baseline;
|
| 305 |
+
2. an extension-owned marker saying a legacy path was accepted;
|
| 306 |
+
3. the legacy baseline.
|
| 307 |
+
|
| 308 |
+
Keeping a legacy change writes the acceptance marker instead of deleting old
|
| 309 |
+
data. Forking materializes the currently effective baseline into the target
|
| 310 |
+
session's extension storage. If the user deletes the legacy home, the migrated
|
| 311 |
+
conversation remains usable but legacy diff/Undo information is no longer
|
| 312 |
+
available.
|
| 313 |
+
|
| 314 |
+
## Packaging and CI
|
| 315 |
+
|
| 316 |
+
The Webview and Extension Host are separate build products:
|
| 317 |
+
|
| 318 |
+
- Vite builds browser assets for the Webview.
|
| 319 |
+
- the Extension Host bundle includes the Node SDK and v1 runtime; only VS Code
|
| 320 |
+
host modules remain external.
|
| 321 |
+
|
| 322 |
+
The production VSIX excludes tests, fixtures, source maps, local profiles,
|
| 323 |
+
sessions, caches, logs, tokens, and the old Python/stdio runtime. Package audit
|
| 324 |
+
checks the manifest, assets, unresolved runtime imports, and sensitive files
|
| 325 |
+
after test state has been created.
|
| 326 |
+
|
| 327 |
+
The extension produces six target artifacts:
|
| 328 |
+
|
| 329 |
+
- `darwin-x64`, `darwin-arm64`
|
| 330 |
+
- `linux-x64`, `linux-arm64`
|
| 331 |
+
- `win32-x64`, `win32-arm64`
|
| 332 |
+
|
| 333 |
+
The CI matrix builds and audits the target VSIX files. Installed-extension smoke
|
| 334 |
+
tests run on Linux, macOS, and Windows x64 runners. Linux checks the declared
|
| 335 |
+
minimum VS Code version and stable; macOS and Windows check the declared
|
| 336 |
+
minimum. Architecture targets without a matching runner remain package/audit
|
| 337 |
+
evidence rather than runtime E2E evidence.
|
| 338 |
+
|
| 339 |
+
Packaging never publishes. Marketplace and Open VSX publication require a
|
| 340 |
+
separate authorized release action.
|
| 341 |
+
|
| 342 |
+
## Validation snapshot
|
| 343 |
+
|
| 344 |
+
The migration was validated with:
|
| 345 |
+
|
| 346 |
+
- package-local tests for the extension, Node SDK, v1 core, and legacy
|
| 347 |
+
migration;
|
| 348 |
+
- real temporary homes and workspaces for migration and baseline behavior;
|
| 349 |
+
- provider-aware model, identity, MCP, error, session replay, delete, and fork
|
| 350 |
+
contract tests;
|
| 351 |
+
- production Extension Host and Webview builds;
|
| 352 |
+
- six target VSIX package audits;
|
| 353 |
+
- an upgrade from the released `0.5.10` extension to local `0.6.0` in an
|
| 354 |
+
isolated profile;
|
| 355 |
+
- installed VSIX Extension Host smoke on Linux, macOS, and Windows x64 CI;
|
| 356 |
+
- local installed `darwin-arm64` VSIX smoke on VS Code `1.100.0`;
|
| 357 |
+
- repository lint, typecheck, tests, Nix build, workspace sync, changeset
|
| 358 |
+
status, and whitespace checks.
|
| 359 |
+
|
| 360 |
+
All automated CI checks for the migration branch were green when this document
|
| 361 |
+
was finalized.
|
| 362 |
+
|
| 363 |
+
## Known limitations and release gates
|
| 364 |
+
|
| 365 |
+
- Remote SSH, WSL, and Dev Container behavior follows `extensionKind:
|
| 366 |
+
["workspace"]` and remote home resolution, but still needs representative
|
| 367 |
+
release-candidate smoke in a real remote environment.
|
| 368 |
+
- Cursor installation, activation, Webview, and basic chat require a usable
|
| 369 |
+
Cursor environment; VS Code smoke is not a substitute.
|
| 370 |
+
- A release candidate should receive one real-account auth/provider smoke in a
|
| 371 |
+
workspace containing no sensitive files.
|
| 372 |
+
- Migrated nested subagents preserve the parent call/summary but cannot recreate
|
| 373 |
+
every child prompt, step, tool, and approval detail from the legacy format.
|
| 374 |
+
- Legacy OAuth and MCP OAuth require reauthorization.
|
| 375 |
+
- Bash-based deletes and arbitrary external file edits are outside baseline
|
| 376 |
+
tracking.
|
| 377 |
+
- Concurrent writes to one session from multiple processes are unsupported.
|
| 378 |
+
- The package script uses a pinned `@vscode/vsce` programmatic `pack()` entry;
|
| 379 |
+
upgrading that package requires revalidating the packaging contract.
|
| 380 |
+
|
| 381 |
+
## Maintenance invariants
|
| 382 |
+
|
| 383 |
+
Future changes must preserve these boundaries unless a new design explicitly
|
| 384 |
+
replaces them:
|
| 385 |
+
|
| 386 |
+
1. The Webview never imports the Node SDK or gains direct Node/file/auth access.
|
| 387 |
+
2. `apps/vscode` never imports engine packages directly.
|
| 388 |
+
3. Shared config and sessions live in the SDK-resolved Kimi Code home; editor
|
| 389 |
+
preferences and baselines remain VS Code-owned.
|
| 390 |
+
4. Legacy migration translation stays in `packages/migration-legacy`.
|
| 391 |
+
5. Session storage is accessed through SDK/core APIs, not parsed or mutated by
|
| 392 |
+
the Webview.
|
| 393 |
+
6. Baselines remain an extension compatibility layer and do not make core tool
|
| 394 |
+
execution wait for VS Code.
|
| 395 |
+
7. Package-only evidence is not reported as runtime E2E evidence.
|
| 396 |
+
8. Build/package commands do not publish artifacts.
|
apps/vscode/package.json
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "kimi-code",
|
| 3 |
+
"publisher": "moonshot-ai",
|
| 4 |
+
"displayName": "Kimi Code",
|
| 5 |
+
"description": "Official Kimi Code plugin for VS Code",
|
| 6 |
+
"version": "0.7.5",
|
| 7 |
+
"private": true,
|
| 8 |
+
"license": "Apache-2.0",
|
| 9 |
+
"type": "module",
|
| 10 |
+
"repository": {
|
| 11 |
+
"type": "git",
|
| 12 |
+
"url": "git+https://github.com/MoonshotAI/kimi-code.git",
|
| 13 |
+
"directory": "apps/vscode"
|
| 14 |
+
},
|
| 15 |
+
"engines": {
|
| 16 |
+
"vscode": "^1.100.0"
|
| 17 |
+
},
|
| 18 |
+
"extensionKind": [
|
| 19 |
+
"workspace"
|
| 20 |
+
],
|
| 21 |
+
"capabilities": {
|
| 22 |
+
"untrustedWorkspaces": {
|
| 23 |
+
"supported": false
|
| 24 |
+
},
|
| 25 |
+
"virtualWorkspaces": false
|
| 26 |
+
},
|
| 27 |
+
"categories": [
|
| 28 |
+
"AI",
|
| 29 |
+
"Chat",
|
| 30 |
+
"Programming Languages"
|
| 31 |
+
],
|
| 32 |
+
"keywords": [
|
| 33 |
+
"ai",
|
| 34 |
+
"assistant",
|
| 35 |
+
"code",
|
| 36 |
+
"chat",
|
| 37 |
+
"agent",
|
| 38 |
+
"mcp",
|
| 39 |
+
"skills",
|
| 40 |
+
"json",
|
| 41 |
+
"autocomplete",
|
| 42 |
+
"moonshot",
|
| 43 |
+
"kimi",
|
| 44 |
+
"moonshot-ai"
|
| 45 |
+
],
|
| 46 |
+
"pricing": "Free",
|
| 47 |
+
"activationEvents": [],
|
| 48 |
+
"main": "./dist/extension.js",
|
| 49 |
+
"icon": "resources/kimi-icon-storefront.png",
|
| 50 |
+
"contributes": {
|
| 51 |
+
"configuration": {
|
| 52 |
+
"title": "Kimi Code",
|
| 53 |
+
"properties": {
|
| 54 |
+
"kimi.yoloMode": {
|
| 55 |
+
"type": "boolean",
|
| 56 |
+
"default": false,
|
| 57 |
+
"description": "Auto-approve regular tool calls; the agent may still ask questions"
|
| 58 |
+
},
|
| 59 |
+
"kimi.autosave": {
|
| 60 |
+
"type": "boolean",
|
| 61 |
+
"default": true,
|
| 62 |
+
"description": "Automatically save files before Kimi reads or writes them"
|
| 63 |
+
},
|
| 64 |
+
"kimi.enableNewConversationShortcut": {
|
| 65 |
+
"type": "boolean",
|
| 66 |
+
"default": false,
|
| 67 |
+
"description": "Use Cmd/Ctrl+N to start a new conversation when Kimi is focused"
|
| 68 |
+
},
|
| 69 |
+
"kimi.useCtrlEnterToSend": {
|
| 70 |
+
"type": "boolean",
|
| 71 |
+
"default": false,
|
| 72 |
+
"description": "Use Ctrl/Cmd+Enter to send prompts instead of Enter"
|
| 73 |
+
},
|
| 74 |
+
"kimi.showThinkingContent": {
|
| 75 |
+
"type": "boolean",
|
| 76 |
+
"default": true,
|
| 77 |
+
"description": "Show thinking/reasoning content in the chat UI"
|
| 78 |
+
},
|
| 79 |
+
"kimi.showThinkingExpanded": {
|
| 80 |
+
"type": "boolean",
|
| 81 |
+
"default": false,
|
| 82 |
+
"description": "Auto-expand thinking/reasoning sections when shown (requires 'Show Thinking Content' to be enabled)"
|
| 83 |
+
},
|
| 84 |
+
"kimi.editorContext": {
|
| 85 |
+
"type": "string",
|
| 86 |
+
"default": "never",
|
| 87 |
+
"enum": [
|
| 88 |
+
"never",
|
| 89 |
+
"onConversationStart",
|
| 90 |
+
"onFileChange"
|
| 91 |
+
],
|
| 92 |
+
"enumDescriptions": [
|
| 93 |
+
"Never share editor context",
|
| 94 |
+
"Share once when conversation starts",
|
| 95 |
+
"Share when active file changes"
|
| 96 |
+
],
|
| 97 |
+
"description": "Control when to share the active editor's file and cursor position with Kimi"
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
"commands": [
|
| 102 |
+
{
|
| 103 |
+
"command": "kimi.clearAllState",
|
| 104 |
+
"title": "Kimi Code: [DEBUG] Clear All State"
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"command": "kimi.openInTab",
|
| 108 |
+
"title": "Kimi Code: Open in New Tab",
|
| 109 |
+
"icon": "$(link-external)"
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"command": "kimi.openInSideBar",
|
| 113 |
+
"title": "Kimi Code: Open in Side Panel",
|
| 114 |
+
"icon": "$(layout-sidebar-left)"
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"command": "kimi.focusInput",
|
| 118 |
+
"title": "Kimi Code: Focus Input",
|
| 119 |
+
"icon": "$(edit)"
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"command": "kimi.insertMention",
|
| 123 |
+
"title": "Kimi Code: Insert Current File",
|
| 124 |
+
"icon": "$(mention)"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"command": "kimi.newConversation",
|
| 128 |
+
"title": "Kimi Code: New Conversation",
|
| 129 |
+
"icon": "$(add)"
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"command": "kimi.showLogs",
|
| 133 |
+
"title": "Kimi Code: Show Logs",
|
| 134 |
+
"icon": "$(output)"
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"command": "kimi.resetKimi",
|
| 138 |
+
"title": "Kimi Code: Reset Kimi (in case of any issues/no response)",
|
| 139 |
+
"icon": "$(refresh)"
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"command": "kimi.logout",
|
| 143 |
+
"title": "Kimi Code: Logout",
|
| 144 |
+
"icon": "$(sign-out)"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"command": "kimi.migrateLegacyData",
|
| 148 |
+
"title": "Kimi Code: Migrate Legacy Data"
|
| 149 |
+
}
|
| 150 |
+
],
|
| 151 |
+
"keybindings": [
|
| 152 |
+
{
|
| 153 |
+
"command": "kimi.focusInput",
|
| 154 |
+
"key": "ctrl+shift+k",
|
| 155 |
+
"mac": "cmd+shift+k"
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"command": "kimi.insertMention",
|
| 159 |
+
"key": "alt+k",
|
| 160 |
+
"mac": "alt+k",
|
| 161 |
+
"when": "editorTextFocus"
|
| 162 |
+
}
|
| 163 |
+
],
|
| 164 |
+
"viewsContainers": {
|
| 165 |
+
"activitybar": [
|
| 166 |
+
{
|
| 167 |
+
"id": "kimi-sidebar",
|
| 168 |
+
"title": "Kimi Code",
|
| 169 |
+
"icon": "resources/kimi-icon.svg"
|
| 170 |
+
}
|
| 171 |
+
]
|
| 172 |
+
},
|
| 173 |
+
"views": {
|
| 174 |
+
"kimi-sidebar": [
|
| 175 |
+
{
|
| 176 |
+
"type": "webview",
|
| 177 |
+
"id": "kimi.webview",
|
| 178 |
+
"name": "Kimi Code"
|
| 179 |
+
}
|
| 180 |
+
]
|
| 181 |
+
},
|
| 182 |
+
"menus": {
|
| 183 |
+
"view/title": [
|
| 184 |
+
{
|
| 185 |
+
"command": "kimi.openInTab",
|
| 186 |
+
"when": "view == kimi.webview",
|
| 187 |
+
"group": "navigation"
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"command": "kimi.newConversation",
|
| 191 |
+
"when": "view == kimi.webview",
|
| 192 |
+
"group": "navigation"
|
| 193 |
+
}
|
| 194 |
+
],
|
| 195 |
+
"commandPalette": [
|
| 196 |
+
{
|
| 197 |
+
"command": "kimi.clearAllState",
|
| 198 |
+
"when": "isDevelopment"
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"command": "kimi.openInTab",
|
| 202 |
+
"when": "true"
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"command": "kimi.openInSideBar",
|
| 206 |
+
"when": "true"
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"command": "kimi.focusInput",
|
| 210 |
+
"when": "true"
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"command": "kimi.insertMention",
|
| 214 |
+
"when": "true"
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"command": "kimi.newConversation",
|
| 218 |
+
"when": "true"
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"command": "kimi.showLogs",
|
| 222 |
+
"when": "true"
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"command": "kimi.resetKimi",
|
| 226 |
+
"when": "true"
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"command": "kimi.logout",
|
| 230 |
+
"when": "kimi.isLoggedIn"
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"command": "kimi.migrateLegacyData",
|
| 234 |
+
"when": "true"
|
| 235 |
+
}
|
| 236 |
+
],
|
| 237 |
+
"editor/context": [
|
| 238 |
+
{
|
| 239 |
+
"command": "kimi.insertMention",
|
| 240 |
+
"group": "moonshot-ai",
|
| 241 |
+
"when": "editorTextFocus"
|
| 242 |
+
}
|
| 243 |
+
]
|
| 244 |
+
}
|
| 245 |
+
},
|
| 246 |
+
"scripts": {
|
| 247 |
+
"vscode:prepublish": "pnpm run build",
|
| 248 |
+
"dev": "pnpm run dev:prepare && pnpm run \"/^dev:(extension|webview)$/\"",
|
| 249 |
+
"dev:prepare": "node scripts/prepare-dev.mjs",
|
| 250 |
+
"dev:extension": "node scripts/watch-extension.mjs",
|
| 251 |
+
"dev:webview": "vite build --config webview-ui/vite.config.ts --watch --sourcemap",
|
| 252 |
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p webview-ui/tsconfig.json --noEmit",
|
| 253 |
+
"build": "pnpm run build:extension && pnpm run build:webview",
|
| 254 |
+
"build:webview": "vite build --config webview-ui/vite.config.ts",
|
| 255 |
+
"build:extension": "tsdown --config tsdown.config.ts",
|
| 256 |
+
"test": "vitest run --config vitest.config.ts",
|
| 257 |
+
"test:extension-host": "node scripts/extension-host-smoke.mjs",
|
| 258 |
+
"package:platform": "node scripts/vsix-package.mjs",
|
| 259 |
+
"package:verify": "node scripts/vsix-verify.mjs",
|
| 260 |
+
"publish:vsix": "node scripts/vsix-publish.mjs",
|
| 261 |
+
"publish:ovsx": "node scripts/ovsx-publish.mjs"
|
| 262 |
+
},
|
| 263 |
+
"devDependencies": {
|
| 264 |
+
"@tailwindcss/vite": "^4.1.4",
|
| 265 |
+
"@testing-library/dom": "^10.4.1",
|
| 266 |
+
"@testing-library/react": "^16.3.3",
|
| 267 |
+
"@testing-library/user-event": "^14.6.6",
|
| 268 |
+
"@types/diff": "^8.0.0",
|
| 269 |
+
"@types/katex": "^0.16.8",
|
| 270 |
+
"@types/node": "^22.15.3",
|
| 271 |
+
"@types/react": "^19.1.2",
|
| 272 |
+
"@types/react-dom": "^19.1.2",
|
| 273 |
+
"@types/react-scroll-to-bottom": "^4.2.5",
|
| 274 |
+
"@types/react-syntax-highlighter": "^15.5.13",
|
| 275 |
+
"@types/vscode": "1.100.0",
|
| 276 |
+
"@vitejs/plugin-react": "^4.4.1",
|
| 277 |
+
"@vscode/test-cli": "^0.0.11",
|
| 278 |
+
"@vscode/test-electron": "^2.5.2",
|
| 279 |
+
"@vscode/vsce": "3.9.2",
|
| 280 |
+
"acorn": "8.17.0",
|
| 281 |
+
"jsdom": "^30.0.1",
|
| 282 |
+
"ovsx": "1.0.2",
|
| 283 |
+
"tailwindcss": "^4.1.4",
|
| 284 |
+
"vite": "^6.3.3",
|
| 285 |
+
"vite-plugin-css-injected-by-js": "^3.5.2",
|
| 286 |
+
"vitest": "4.1.4"
|
| 287 |
+
},
|
| 288 |
+
"dependencies": {
|
| 289 |
+
"@base-ui/react": "^1.0.0",
|
| 290 |
+
"@fontsource-variable/inter": "5.2.8",
|
| 291 |
+
"@moonshot-ai/kimi-code-oauth": "workspace:^",
|
| 292 |
+
"@moonshot-ai/kimi-code-sdk": "workspace:^",
|
| 293 |
+
"@moonshot-ai/migration-legacy": "workspace:^",
|
| 294 |
+
"@radix-ui/react-accordion": "^1.2.12",
|
| 295 |
+
"@tabler/icons-react": "^3.36.0",
|
| 296 |
+
"@tanstack/react-query": "^5.74.4",
|
| 297 |
+
"class-variance-authority": "^0.7.1",
|
| 298 |
+
"clsx": "^2.1.1",
|
| 299 |
+
"cmdk": "^1.1.1",
|
| 300 |
+
"diff": "^8.0.2",
|
| 301 |
+
"fuse.js": "^7.1.0",
|
| 302 |
+
"heic-to": "^1.0.2",
|
| 303 |
+
"immer": "^11.1.0",
|
| 304 |
+
"katex": "^0.17.0",
|
| 305 |
+
"next-themes": "^0.4.6",
|
| 306 |
+
"radix-ui": "^1.4.3",
|
| 307 |
+
"react": "^19.1.0",
|
| 308 |
+
"react-dom": "^19.1.0",
|
| 309 |
+
"react-markdown": "^10.1.0",
|
| 310 |
+
"react-scroll-to-bottom": "^4.2.0",
|
| 311 |
+
"react-syntax-highlighter": "^16.1.0",
|
| 312 |
+
"rehype-katex": "^7.0.1",
|
| 313 |
+
"remark-gfm": "^4.0.1",
|
| 314 |
+
"remark-math": "^6.0.0",
|
| 315 |
+
"shadcn": "^3.6.2",
|
| 316 |
+
"sonner": "^2.0.7",
|
| 317 |
+
"tailwind-merge": "^3.4.0",
|
| 318 |
+
"tw-animate-css": "^1.4.0",
|
| 319 |
+
"zustand": "^5"
|
| 320 |
+
}
|
| 321 |
+
}
|
apps/vscode/resources/kimi-icon-storefront.png
ADDED
|
|
apps/vscode/resources/kimi-icon.svg
ADDED
|
|
apps/vscode/scripts/extension-host-smoke.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface ExtensionHostSmokeOptions {
|
| 2 |
+
version?: string;
|
| 3 |
+
vsixPath?: string;
|
| 4 |
+
cachePath?: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export interface ExtensionHostSmokeResult {
|
| 8 |
+
version: string;
|
| 9 |
+
vscodeVersion: string;
|
| 10 |
+
vsixPath: string;
|
| 11 |
+
cachePath: string;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export function runExtensionHostSmoke(
|
| 15 |
+
options?: ExtensionHostSmokeOptions,
|
| 16 |
+
): Promise<ExtensionHostSmokeResult>;
|
apps/vscode/scripts/local-cli.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { spawnSync } from 'node:child_process';
|
| 2 |
+
import { readFileSync } from 'node:fs';
|
| 3 |
+
import { dirname, resolve } from 'node:path';
|
| 4 |
+
import { createRequire } from 'node:module';
|
| 5 |
+
|
| 6 |
+
const require = createRequire(import.meta.url);
|
| 7 |
+
|
| 8 |
+
export function resolveLocalCli(packageName, executableName) {
|
| 9 |
+
let packageJsonPath;
|
| 10 |
+
try {
|
| 11 |
+
packageJsonPath = require.resolve(`${packageName}/package.json`);
|
| 12 |
+
} catch (error) {
|
| 13 |
+
throw new Error(
|
| 14 |
+
`Local CLI dependency ${packageName} is not installed. Run pnpm install; runtime CLI downloads are disabled.`,
|
| 15 |
+
{ cause: error },
|
| 16 |
+
);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
| 20 |
+
const relativeBin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[executableName];
|
| 21 |
+
if (typeof relativeBin !== 'string') {
|
| 22 |
+
throw new Error(`${packageName} does not declare the expected "${executableName}" binary.`);
|
| 23 |
+
}
|
| 24 |
+
return resolve(dirname(packageJsonPath), relativeBin);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function runLocalCli(packageName, executableName, args, options = {}) {
|
| 28 |
+
const cliPath = resolveLocalCli(packageName, executableName);
|
| 29 |
+
const result = spawnSync(process.execPath, [cliPath, ...args], {
|
| 30 |
+
cwd: options.cwd,
|
| 31 |
+
env: options.env ?? process.env,
|
| 32 |
+
encoding: options.encoding,
|
| 33 |
+
stdio: options.stdio ?? 'inherit',
|
| 34 |
+
});
|
| 35 |
+
if (result.error !== undefined) {
|
| 36 |
+
throw new Error(`Unable to start local ${executableName}: ${result.error.message}`, {
|
| 37 |
+
cause: result.error,
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
if (result.status !== 0) {
|
| 41 |
+
const output = [result.stdout, result.stderr].filter(Boolean).join('');
|
| 42 |
+
throw new Error(
|
| 43 |
+
`Local ${executableName} exited with code ${result.status ?? 'unknown'}${output ? `:\n${output}` : ''}`,
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
return result;
|
| 47 |
+
}
|
apps/vscode/scripts/ovsx-publish.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
import { existsSync } from 'node:fs';
|
| 3 |
+
|
| 4 |
+
import { runLocalCli } from './local-cli.mjs';
|
| 5 |
+
import { parsePublishArguments, publishUsage } from './publish-args.mjs';
|
| 6 |
+
import { extensionRoot, isMainModule } from './vsix-targets.mjs';
|
| 7 |
+
import { verifyVsix } from './vsix-verify.mjs';
|
| 8 |
+
|
| 9 |
+
async function main() {
|
| 10 |
+
const options = parsePublishArguments(process.argv.slice(2));
|
| 11 |
+
if (options.help) {
|
| 12 |
+
console.log(publishUsage('Open VSX'));
|
| 13 |
+
return;
|
| 14 |
+
}
|
| 15 |
+
if (!process.env.OVSX_PAT) throw new Error('OVSX_PAT is required to publish.');
|
| 16 |
+
|
| 17 |
+
await verifyInputs(options);
|
| 18 |
+
for (const file of options.files) {
|
| 19 |
+
console.log(`Publishing verified package ${file}...`);
|
| 20 |
+
try {
|
| 21 |
+
runLocalCli('ovsx', 'ovsx', ['publish', file], {
|
| 22 |
+
cwd: extensionRoot,
|
| 23 |
+
encoding: 'utf8',
|
| 24 |
+
stdio: 'pipe',
|
| 25 |
+
});
|
| 26 |
+
} catch (error) {
|
| 27 |
+
if (/already exists/i.test(error instanceof Error ? error.message : String(error))) {
|
| 28 |
+
console.log(`Package already exists: ${file}`);
|
| 29 |
+
continue;
|
| 30 |
+
}
|
| 31 |
+
throw error;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function verifyInputs(options) {
|
| 37 |
+
for (let index = 0; index < options.targets.length; index += 1) {
|
| 38 |
+
const file = options.files[index];
|
| 39 |
+
if (!existsSync(file)) {
|
| 40 |
+
throw new Error(`Missing VSIX ${file}. Run pnpm run package:platform first.`);
|
| 41 |
+
}
|
| 42 |
+
await verifyVsix(file, options.targets[index], { sourceRoot: extensionRoot });
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
if (isMainModule(import.meta.url)) {
|
| 47 |
+
main().catch((error) => {
|
| 48 |
+
console.error(`Open VSX publish failed: ${error instanceof Error ? error.message : String(error)}`);
|
| 49 |
+
process.exitCode = 1;
|
| 50 |
+
});
|
| 51 |
+
}
|
apps/vscode/scripts/prepare-dev.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
| 3 |
+
import { basename, dirname, join, parse, resolve } from 'node:path';
|
| 4 |
+
import { fileURLToPath } from 'node:url';
|
| 5 |
+
|
| 6 |
+
import { isMainModule } from './vsix-targets.mjs';
|
| 7 |
+
|
| 8 |
+
const SAFE_DIRECTORY_NAME = 'vscode-extension-dev';
|
| 9 |
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
| 10 |
+
const monorepoRoot = resolve(scriptDir, '../../..');
|
| 11 |
+
const defaultBaseDir = join(monorepoRoot, '.tmp', SAFE_DIRECTORY_NAME);
|
| 12 |
+
|
| 13 |
+
export async function prepareDevEnvironment(baseDir = defaultBaseDir) {
|
| 14 |
+
const root = resolve(baseDir);
|
| 15 |
+
assertSafeRoot(root);
|
| 16 |
+
await rm(root, { recursive: true, force: true });
|
| 17 |
+
|
| 18 |
+
const paths = {
|
| 19 |
+
root,
|
| 20 |
+
userData: join(root, 'user-data'),
|
| 21 |
+
extensions: join(root, 'extensions'),
|
| 22 |
+
kimiHome: join(root, 'kimi-home'),
|
| 23 |
+
workspace: join(root, 'workspace'),
|
| 24 |
+
};
|
| 25 |
+
await Promise.all(
|
| 26 |
+
Object.values(paths)
|
| 27 |
+
.filter((path) => path !== root)
|
| 28 |
+
.map((path) => mkdir(path, { recursive: true })),
|
| 29 |
+
);
|
| 30 |
+
await writeFile(
|
| 31 |
+
join(paths.workspace, 'README.md'),
|
| 32 |
+
'# Isolated Kimi Code extension development workspace\n',
|
| 33 |
+
);
|
| 34 |
+
return paths;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function assertSafeRoot(root) {
|
| 38 |
+
const parsed = parse(root);
|
| 39 |
+
if (root === parsed.root || basename(root) !== SAFE_DIRECTORY_NAME) {
|
| 40 |
+
throw new Error(
|
| 41 |
+
`Refusing to reset unsafe development directory "${root}"; it must end in ${SAFE_DIRECTORY_NAME}.`,
|
| 42 |
+
);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
function parseArguments(argv) {
|
| 47 |
+
let baseDir = defaultBaseDir;
|
| 48 |
+
let help = false;
|
| 49 |
+
for (let index = 0; index < argv.length; index += 1) {
|
| 50 |
+
const argument = argv[index];
|
| 51 |
+
if (argument === '--') {
|
| 52 |
+
continue;
|
| 53 |
+
} else if (argument === '--help' || argument === '-h') {
|
| 54 |
+
help = true;
|
| 55 |
+
} else if (argument === '--base-dir') {
|
| 56 |
+
const value = argv[++index];
|
| 57 |
+
if (value === undefined || value.startsWith('-')) throw new Error('--base-dir requires a value.');
|
| 58 |
+
baseDir = value;
|
| 59 |
+
} else {
|
| 60 |
+
throw new Error(`Unknown option: ${argument}`);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
return { baseDir, help };
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
async function main() {
|
| 67 |
+
const options = parseArguments(process.argv.slice(2));
|
| 68 |
+
if (options.help) {
|
| 69 |
+
console.log('Usage: node scripts/prepare-dev.mjs [--base-dir <.../vscode-extension-dev>]');
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
const paths = await prepareDevEnvironment(options.baseDir);
|
| 73 |
+
console.log(`Prepared isolated VS Code profile: ${paths.root}`);
|
| 74 |
+
console.log(`KIMI_CODE_HOME=${paths.kimiHome}`);
|
| 75 |
+
console.log(`Workspace=${paths.workspace}`);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if (isMainModule(import.meta.url)) {
|
| 79 |
+
main().catch((error) => {
|
| 80 |
+
console.error(`Development environment setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
| 81 |
+
process.exitCode = 1;
|
| 82 |
+
});
|
| 83 |
+
}
|
apps/vscode/scripts/publish-args.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { join, resolve } from 'node:path';
|
| 2 |
+
|
| 3 |
+
import {
|
| 4 |
+
defaultVsixOutputDir,
|
| 5 |
+
normalizeVsixTargets,
|
| 6 |
+
vsixFileName,
|
| 7 |
+
} from './vsix-targets.mjs';
|
| 8 |
+
|
| 9 |
+
export function parsePublishArguments(argv) {
|
| 10 |
+
const targets = [];
|
| 11 |
+
let outputDir = defaultVsixOutputDir;
|
| 12 |
+
let help = false;
|
| 13 |
+
|
| 14 |
+
for (let index = 0; index < argv.length; index += 1) {
|
| 15 |
+
const argument = argv[index];
|
| 16 |
+
if (argument === '--') {
|
| 17 |
+
continue;
|
| 18 |
+
} else if (argument === '--help' || argument === '-h') {
|
| 19 |
+
help = true;
|
| 20 |
+
} else if (argument === '--out-dir') {
|
| 21 |
+
outputDir = requireOptionValue(argv, ++index, '--out-dir');
|
| 22 |
+
} else if (argument === '--target') {
|
| 23 |
+
targets.push(requireOptionValue(argv, ++index, '--target'));
|
| 24 |
+
} else if (argument.startsWith('-')) {
|
| 25 |
+
throw new Error(`Unknown option: ${argument}`);
|
| 26 |
+
} else {
|
| 27 |
+
targets.push(argument);
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const normalizedTargets = normalizeVsixTargets(targets);
|
| 32 |
+
const resolvedOutputDir = resolve(outputDir);
|
| 33 |
+
return {
|
| 34 |
+
help,
|
| 35 |
+
targets: normalizedTargets,
|
| 36 |
+
outputDir: resolvedOutputDir,
|
| 37 |
+
files: normalizedTargets.map((target) => join(resolvedOutputDir, vsixFileName(target))),
|
| 38 |
+
};
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function requireOptionValue(argv, index, option) {
|
| 42 |
+
const value = argv[index];
|
| 43 |
+
if (value !== undefined && !value.startsWith('-')) return value;
|
| 44 |
+
throw new Error(`${option} requires a value.`);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export function publishUsage(marketplace) {
|
| 48 |
+
return [
|
| 49 |
+
`Usage: node scripts/${marketplace === 'Open VSX' ? 'ovsx' : 'vsix'}-publish.mjs [targets...] [--out-dir <directory>]`,
|
| 50 |
+
'',
|
| 51 |
+
`Publishes already-built, re-verified VSIX files to ${marketplace}.`,
|
| 52 |
+
'This command never builds or downloads a CLI.',
|
| 53 |
+
].join('\n');
|
| 54 |
+
}
|
apps/vscode/scripts/vsix-package.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
import { createRequire } from 'node:module';
|
| 3 |
+
import { mkdir } from 'node:fs/promises';
|
| 4 |
+
import { join, resolve } from 'node:path';
|
| 5 |
+
|
| 6 |
+
import { runLocalCli } from './local-cli.mjs';
|
| 7 |
+
import {
|
| 8 |
+
defaultVsixOutputDir,
|
| 9 |
+
extensionRoot,
|
| 10 |
+
isMainModule,
|
| 11 |
+
normalizeVsixTargets,
|
| 12 |
+
vsixFileName,
|
| 13 |
+
} from './vsix-targets.mjs';
|
| 14 |
+
import { verifyVsix } from './vsix-verify.mjs';
|
| 15 |
+
|
| 16 |
+
const require = createRequire(import.meta.url);
|
| 17 |
+
|
| 18 |
+
function parseArguments(argv) {
|
| 19 |
+
const targets = [];
|
| 20 |
+
let outputDir = defaultVsixOutputDir;
|
| 21 |
+
let dryRun = false;
|
| 22 |
+
let help = false;
|
| 23 |
+
|
| 24 |
+
for (let index = 0; index < argv.length; index += 1) {
|
| 25 |
+
const argument = argv[index];
|
| 26 |
+
if (argument === '--') {
|
| 27 |
+
continue;
|
| 28 |
+
} else if (argument === '--help' || argument === '-h') {
|
| 29 |
+
help = true;
|
| 30 |
+
} else if (argument === '--dry-run') {
|
| 31 |
+
dryRun = true;
|
| 32 |
+
} else if (argument === '--out-dir') {
|
| 33 |
+
outputDir = requireOptionValue(argv, ++index, '--out-dir');
|
| 34 |
+
} else if (argument === '--target') {
|
| 35 |
+
targets.push(requireOptionValue(argv, ++index, '--target'));
|
| 36 |
+
} else if (argument.startsWith('-')) {
|
| 37 |
+
throw new Error(`Unknown option: ${argument}`);
|
| 38 |
+
} else {
|
| 39 |
+
targets.push(argument);
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return {
|
| 44 |
+
targets: normalizeVsixTargets(targets),
|
| 45 |
+
outputDir: resolve(outputDir),
|
| 46 |
+
dryRun,
|
| 47 |
+
help,
|
| 48 |
+
};
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function requireOptionValue(argv, index, option) {
|
| 52 |
+
const value = argv[index];
|
| 53 |
+
if (value !== undefined && !value.startsWith('-')) return value;
|
| 54 |
+
throw new Error(`${option} requires a value.`);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function usage() {
|
| 58 |
+
return [
|
| 59 |
+
'Usage: node scripts/vsix-package.mjs [targets...] [--out-dir <directory>]',
|
| 60 |
+
' node scripts/vsix-package.mjs --target win32-x64 --dry-run',
|
| 61 |
+
'',
|
| 62 |
+
'With no targets, all six supported VSIX targets are built and audited.',
|
| 63 |
+
'This command never publishes an extension.',
|
| 64 |
+
].join('\n');
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
async function main() {
|
| 68 |
+
const options = parseArguments(process.argv.slice(2));
|
| 69 |
+
if (options.help) {
|
| 70 |
+
console.log(usage());
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
await mkdir(options.outputDir, { recursive: true });
|
| 75 |
+
console.log(`VSIX targets: ${options.targets.join(', ')}`);
|
| 76 |
+
console.log(`Output directory: ${options.outputDir}`);
|
| 77 |
+
|
| 78 |
+
if (!options.dryRun) {
|
| 79 |
+
console.log('Building the extension once before packaging all targets...');
|
| 80 |
+
buildExtension();
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const packVsix = options.dryRun ? undefined : loadVscePack();
|
| 84 |
+
|
| 85 |
+
for (const target of options.targets) {
|
| 86 |
+
const outputPath = join(options.outputDir, vsixFileName(target));
|
| 87 |
+
if (options.dryRun) {
|
| 88 |
+
console.log(`Would package ${target} -> ${outputPath}`);
|
| 89 |
+
continue;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
console.log(`\nPackaging ${target} with the workspace @vscode/vsce...`);
|
| 93 |
+
await packVsix({
|
| 94 |
+
cwd: extensionRoot,
|
| 95 |
+
dependencies: false,
|
| 96 |
+
packagePath: outputPath,
|
| 97 |
+
target,
|
| 98 |
+
});
|
| 99 |
+
const result = await verifyVsix(outputPath, target, { sourceRoot: extensionRoot });
|
| 100 |
+
console.log(
|
| 101 |
+
`Verified ${target}: ${result.files} files, ${result.bytes} unpacked bytes; package/static checks passed.`,
|
| 102 |
+
);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
if (!options.dryRun) {
|
| 106 |
+
console.log(
|
| 107 |
+
'\nVSIX packaging complete. These are package-only results until each target runs in its matching extension host.',
|
| 108 |
+
);
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function buildExtension() {
|
| 113 |
+
runLocalCli('tsdown', 'tsdown', ['--config', 'tsdown.config.ts'], { cwd: extensionRoot });
|
| 114 |
+
runLocalCli('vite', 'vite', ['build', '--config', 'webview-ui/vite.config.ts'], {
|
| 115 |
+
cwd: extensionRoot,
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
function loadVscePack() {
|
| 120 |
+
try {
|
| 121 |
+
// VSCE's public API always runs vscode:prepublish. We build once above and pin VSCE,
|
| 122 |
+
// so use its package phase directly instead of rebuilding for every target.
|
| 123 |
+
const module = require('@vscode/vsce/out/package.js');
|
| 124 |
+
if (typeof module.pack !== 'function') {
|
| 125 |
+
throw new TypeError('the installed package does not expose pack()');
|
| 126 |
+
}
|
| 127 |
+
return module.pack;
|
| 128 |
+
} catch (error) {
|
| 129 |
+
throw new Error(
|
| 130 |
+
'The workspace @vscode/vsce package API is unavailable. Run pnpm install; runtime CLI downloads are disabled.',
|
| 131 |
+
{ cause: error },
|
| 132 |
+
);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
if (isMainModule(import.meta.url)) {
|
| 137 |
+
main().catch((error) => {
|
| 138 |
+
console.error(`VSIX packaging failed: ${error instanceof Error ? error.message : String(error)}`);
|
| 139 |
+
process.exitCode = 1;
|
| 140 |
+
});
|
| 141 |
+
}
|
apps/vscode/scripts/vsix-publish.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
import { existsSync } from 'node:fs';
|
| 3 |
+
|
| 4 |
+
import { runLocalCli } from './local-cli.mjs';
|
| 5 |
+
import { parsePublishArguments, publishUsage } from './publish-args.mjs';
|
| 6 |
+
import { extensionRoot, isMainModule } from './vsix-targets.mjs';
|
| 7 |
+
import { verifyVsix } from './vsix-verify.mjs';
|
| 8 |
+
|
| 9 |
+
async function main() {
|
| 10 |
+
const options = parsePublishArguments(process.argv.slice(2));
|
| 11 |
+
if (options.help) {
|
| 12 |
+
console.log(publishUsage('Visual Studio Marketplace'));
|
| 13 |
+
return;
|
| 14 |
+
}
|
| 15 |
+
if (!process.env.VSCE_PAT) throw new Error('VSCE_PAT is required to publish.');
|
| 16 |
+
|
| 17 |
+
await verifyInputs(options);
|
| 18 |
+
for (const file of options.files) {
|
| 19 |
+
console.log(`Publishing verified package ${file}...`);
|
| 20 |
+
runLocalCli(
|
| 21 |
+
'@vscode/vsce',
|
| 22 |
+
'vsce',
|
| 23 |
+
['publish', '--packagePath', file, '--skip-duplicate'],
|
| 24 |
+
{ cwd: extensionRoot },
|
| 25 |
+
);
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async function verifyInputs(options) {
|
| 30 |
+
for (let index = 0; index < options.targets.length; index += 1) {
|
| 31 |
+
const file = options.files[index];
|
| 32 |
+
if (!existsSync(file)) {
|
| 33 |
+
throw new Error(`Missing VSIX ${file}. Run pnpm run package:platform first.`);
|
| 34 |
+
}
|
| 35 |
+
await verifyVsix(file, options.targets[index], { sourceRoot: extensionRoot });
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
if (isMainModule(import.meta.url)) {
|
| 40 |
+
main().catch((error) => {
|
| 41 |
+
console.error(`Marketplace publish failed: ${error instanceof Error ? error.message : String(error)}`);
|
| 42 |
+
process.exitCode = 1;
|
| 43 |
+
});
|
| 44 |
+
}
|
apps/vscode/scripts/vsix-targets.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { dirname, join, resolve } from 'node:path';
|
| 2 |
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
| 3 |
+
|
| 4 |
+
export const VSIX_TARGETS = Object.freeze([
|
| 5 |
+
'darwin-x64',
|
| 6 |
+
'darwin-arm64',
|
| 7 |
+
'linux-x64',
|
| 8 |
+
'linux-arm64',
|
| 9 |
+
'win32-x64',
|
| 10 |
+
'win32-arm64',
|
| 11 |
+
]);
|
| 12 |
+
|
| 13 |
+
export const extensionRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
| 14 |
+
export const defaultVsixOutputDir = join(extensionRoot, 'artifacts', 'vsix');
|
| 15 |
+
|
| 16 |
+
export function vsixFileName(target) {
|
| 17 |
+
assertVsixTarget(target);
|
| 18 |
+
return `kimi-code-${target}.vsix`;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export function normalizeVsixTargets(values) {
|
| 22 |
+
const requested = values.length === 0 || values.includes('all') ? VSIX_TARGETS : values;
|
| 23 |
+
if (values.includes('all') && values.length > 1) {
|
| 24 |
+
throw new Error('Target "all" cannot be combined with an explicit VSIX target.');
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const targets = [];
|
| 28 |
+
for (const target of requested) {
|
| 29 |
+
assertVsixTarget(target);
|
| 30 |
+
if (!targets.includes(target)) targets.push(target);
|
| 31 |
+
}
|
| 32 |
+
return targets;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export function assertVsixTarget(target) {
|
| 36 |
+
if (VSIX_TARGETS.includes(target)) return;
|
| 37 |
+
throw new Error(
|
| 38 |
+
`Unknown VSIX target "${target}". Expected one of: ${VSIX_TARGETS.join(', ')}`,
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export function isMainModule(metaUrl) {
|
| 43 |
+
const argvPath = process.argv[1];
|
| 44 |
+
return argvPath !== undefined && pathToFileURL(resolve(argvPath)).href === metaUrl;
|
| 45 |
+
}
|
apps/vscode/scripts/vsix-verify.mjs
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
import { builtinModules } from 'node:module';
|
| 3 |
+
import { spawnSync } from 'node:child_process';
|
| 4 |
+
import { existsSync } from 'node:fs';
|
| 5 |
+
import {
|
| 6 |
+
mkdtemp,
|
| 7 |
+
mkdir,
|
| 8 |
+
readFile,
|
| 9 |
+
readdir,
|
| 10 |
+
rm,
|
| 11 |
+
stat,
|
| 12 |
+
writeFile,
|
| 13 |
+
} from 'node:fs/promises';
|
| 14 |
+
import { homedir, tmpdir } from 'node:os';
|
| 15 |
+
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
| 16 |
+
import { pathToFileURL } from 'node:url';
|
| 17 |
+
import { isDeepStrictEqual } from 'node:util';
|
| 18 |
+
import { parse } from 'acorn';
|
| 19 |
+
|
| 20 |
+
import { extractZip } from './zip.mjs';
|
| 21 |
+
import {
|
| 22 |
+
defaultVsixOutputDir,
|
| 23 |
+
extensionRoot,
|
| 24 |
+
isMainModule,
|
| 25 |
+
normalizeVsixTargets,
|
| 26 |
+
vsixFileName,
|
| 27 |
+
} from './vsix-targets.mjs';
|
| 28 |
+
|
| 29 |
+
const REQUIRED_WEBVIEW_FILES = [
|
| 30 |
+
'dist/webview.js',
|
| 31 |
+
'dist/kimi-banner-dark.svg',
|
| 32 |
+
'dist/kimi-banner-light.svg',
|
| 33 |
+
'dist/kimi-logo.png',
|
| 34 |
+
];
|
| 35 |
+
const FORBIDDEN_PATH_SEGMENTS = new Set([
|
| 36 |
+
'.kimi',
|
| 37 |
+
'.kimi-code',
|
| 38 |
+
'.vscode',
|
| 39 |
+
'__tests__',
|
| 40 |
+
'cache',
|
| 41 |
+
'caches',
|
| 42 |
+
'credentials',
|
| 43 |
+
'logs',
|
| 44 |
+
'node_modules',
|
| 45 |
+
'profile',
|
| 46 |
+
'profiles',
|
| 47 |
+
'runtime',
|
| 48 |
+
'scripts',
|
| 49 |
+
'session',
|
| 50 |
+
'sessions',
|
| 51 |
+
'src',
|
| 52 |
+
'state',
|
| 53 |
+
'states',
|
| 54 |
+
'test',
|
| 55 |
+
'tests',
|
| 56 |
+
'tokens',
|
| 57 |
+
'webview-ui',
|
| 58 |
+
]);
|
| 59 |
+
const FORBIDDEN_EXTENSIONS = new Set(['.jsonl', '.log', '.map', '.py', '.pyc', '.ts', '.tsx']);
|
| 60 |
+
const TEXT_EXTENSIONS = new Set([
|
| 61 |
+
'.cjs',
|
| 62 |
+
'.css',
|
| 63 |
+
'.html',
|
| 64 |
+
'.js',
|
| 65 |
+
'.json',
|
| 66 |
+
'.md',
|
| 67 |
+
'.mjs',
|
| 68 |
+
'.svg',
|
| 69 |
+
'.txt',
|
| 70 |
+
'.xml',
|
| 71 |
+
]);
|
| 72 |
+
const BUILTIN_IMPORTS = new Set(
|
| 73 |
+
builtinModules.flatMap((name) => [name, name.startsWith('node:') ? name.slice(5) : `node:${name}`]),
|
| 74 |
+
);
|
| 75 |
+
// `ws` probes these native accelerators inside try/catch and immediately uses
|
| 76 |
+
// its bundled JavaScript fallback when they are absent. They are not required
|
| 77 |
+
// runtime dependencies and must not be shipped as cross-platform native code.
|
| 78 |
+
const OPTIONAL_FALLBACK_IMPORTS = new Set(['bufferutil', 'canvas', 'utf-8-validate']);
|
| 79 |
+
const MANIFEST_FIELDS = [
|
| 80 |
+
'name',
|
| 81 |
+
'publisher',
|
| 82 |
+
'displayName',
|
| 83 |
+
'version',
|
| 84 |
+
'engines',
|
| 85 |
+
'extensionKind',
|
| 86 |
+
'capabilities',
|
| 87 |
+
'activationEvents',
|
| 88 |
+
'main',
|
| 89 |
+
'icon',
|
| 90 |
+
];
|
| 91 |
+
const CONTRIBUTE_FIELDS = [
|
| 92 |
+
'commands',
|
| 93 |
+
'configuration',
|
| 94 |
+
'keybindings',
|
| 95 |
+
'menus',
|
| 96 |
+
'views',
|
| 97 |
+
'viewsContainers',
|
| 98 |
+
];
|
| 99 |
+
|
| 100 |
+
export async function verifyVsix(vsixPath, target, options = {}) {
|
| 101 |
+
const extractionRoot = await mkdtemp(join(tmpdir(), 'kimi-vsix-audit-'));
|
| 102 |
+
try {
|
| 103 |
+
await extractZip(vsixPath, extractionRoot);
|
| 104 |
+
return await auditExtractedVsix(extractionRoot, target, options);
|
| 105 |
+
} finally {
|
| 106 |
+
await rm(extractionRoot, { recursive: true, force: true });
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
export async function auditExtractedVsix(extractionRoot, target, options = {}) {
|
| 111 |
+
const sourceRoot = options.sourceRoot ?? extensionRoot;
|
| 112 |
+
const extensionDir = join(extractionRoot, 'extension');
|
| 113 |
+
const files = await listFiles(extractionRoot);
|
| 114 |
+
const fileSet = new Set(files);
|
| 115 |
+
|
| 116 |
+
requireFile(fileSet, 'extension.vsixmanifest');
|
| 117 |
+
requireFile(fileSet, '[Content_Types].xml');
|
| 118 |
+
requireFile(fileSet, 'extension/package.json');
|
| 119 |
+
const packagedManifest = await readJson(join(extensionDir, 'package.json'), 'package.json');
|
| 120 |
+
const sourceManifest = await readJson(join(sourceRoot, 'package.json'), 'source package.json');
|
| 121 |
+
|
| 122 |
+
await verifyTargetManifest(extractionRoot, target);
|
| 123 |
+
verifyPackageManifest(packagedManifest, sourceManifest);
|
| 124 |
+
verifyRequiredFiles(fileSet, packagedManifest);
|
| 125 |
+
verifyForbiddenFiles(files);
|
| 126 |
+
await verifyNoSensitiveContent(extractionRoot, files, sourceRoot, options.forbiddenText ?? []);
|
| 127 |
+
await verifyRuntimeImports(extensionDir, files);
|
| 128 |
+
await verifyEntryImport(extensionDir, packagedManifest.main);
|
| 129 |
+
|
| 130 |
+
const bytes = await totalSize(extractionRoot, files);
|
| 131 |
+
return { target, files: files.length, bytes };
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
async function verifyTargetManifest(extractionRoot, target) {
|
| 135 |
+
const xml = await readFile(join(extractionRoot, 'extension.vsixmanifest'), 'utf8');
|
| 136 |
+
const actual = xml.match(/\bTargetPlatform="([^"]+)"/)?.[1];
|
| 137 |
+
if (actual !== target) {
|
| 138 |
+
throw new Error(
|
| 139 |
+
`VSIX manifest target is ${actual ?? 'missing'}, expected ${target}.`,
|
| 140 |
+
);
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function verifyPackageManifest(packaged, source) {
|
| 145 |
+
if (typeof packaged.main !== 'string' || !packaged.main.endsWith('.js')) {
|
| 146 |
+
throw new Error(`Packaged extension main must be a .js entry, got ${String(packaged.main)}.`);
|
| 147 |
+
}
|
| 148 |
+
if (packaged.main !== './dist/extension.js') {
|
| 149 |
+
throw new Error(`Packaged extension main is ${packaged.main}, expected ./dist/extension.js.`);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
for (const field of MANIFEST_FIELDS) {
|
| 153 |
+
if (!isDeepStrictEqual(packaged[field], source[field])) {
|
| 154 |
+
throw new Error(`Packaged package.json field "${field}" does not match the source manifest.`);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
for (const field of CONTRIBUTE_FIELDS) {
|
| 158 |
+
if (!isDeepStrictEqual(packaged.contributes?.[field], source.contributes?.[field])) {
|
| 159 |
+
throw new Error(
|
| 160 |
+
`Packaged package.json contributes.${field} does not match the source manifest.`,
|
| 161 |
+
);
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
function verifyRequiredFiles(fileSet, manifest) {
|
| 167 |
+
const required = [
|
| 168 |
+
'extension/LICENSE.txt',
|
| 169 |
+
`extension/${stripLeadingDotSlash(manifest.main)}`,
|
| 170 |
+
...REQUIRED_WEBVIEW_FILES.map((file) => `extension/${file}`),
|
| 171 |
+
];
|
| 172 |
+
requireOneOf(fileSet, ['extension/README.md', 'extension/readme.md'], 'marketplace README');
|
| 173 |
+
if (typeof manifest.icon === 'string') required.push(`extension/${manifest.icon}`);
|
| 174 |
+
|
| 175 |
+
for (const container of Object.values(manifest.contributes?.viewsContainers ?? {})) {
|
| 176 |
+
if (!Array.isArray(container)) continue;
|
| 177 |
+
for (const view of container) {
|
| 178 |
+
if (typeof view?.icon === 'string') required.push(`extension/${view.icon}`);
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
for (const file of required) requireFile(fileSet, file);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function verifyForbiddenFiles(files) {
|
| 185 |
+
for (const file of files) {
|
| 186 |
+
const normalized = file.replaceAll('\\', '/');
|
| 187 |
+
const lower = normalized.toLowerCase();
|
| 188 |
+
const extensionRelative = lower.startsWith('extension/') ? lower.slice('extension/'.length) : lower;
|
| 189 |
+
const segments = extensionRelative.split('/');
|
| 190 |
+
const forbiddenSegment = segments.find((segment) => FORBIDDEN_PATH_SEGMENTS.has(segment));
|
| 191 |
+
if (forbiddenSegment !== undefined) {
|
| 192 |
+
throw new Error(`Forbidden package path segment "${forbiddenSegment}" in ${normalized}.`);
|
| 193 |
+
}
|
| 194 |
+
if (FORBIDDEN_EXTENSIONS.has(extname(lower))) {
|
| 195 |
+
throw new Error(`Forbidden package file type in ${normalized}.`);
|
| 196 |
+
}
|
| 197 |
+
if (
|
| 198 |
+
lower.includes('kimi-agent-sdk') ||
|
| 199 |
+
lower.includes('download-cli') ||
|
| 200 |
+
lower.includes('/bin/kimi/') ||
|
| 201 |
+
/(^|\/)uv(?:\.exe)?$/.test(lower)
|
| 202 |
+
) {
|
| 203 |
+
throw new Error(`Legacy CLI/runtime artifact found in ${normalized}.`);
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
async function verifyNoSensitiveContent(extractionRoot, files, sourceRoot, extraForbiddenText) {
|
| 209 |
+
const secretValues = [process.env.VSCE_PAT, process.env.OVSX_PAT]
|
| 210 |
+
.filter((value) => typeof value === 'string' && value.length >= 8);
|
| 211 |
+
const forbidden = [sourceRoot, homedir(), ...extraForbiddenText, ...secretValues]
|
| 212 |
+
.filter((value) => typeof value === 'string' && value.length >= 4)
|
| 213 |
+
.flatMap((value) => [value, value.replaceAll('\\', '/'), value.replaceAll('/', '\\')]);
|
| 214 |
+
|
| 215 |
+
for (const file of files) {
|
| 216 |
+
if (!TEXT_EXTENSIONS.has(extname(file).toLowerCase())) continue;
|
| 217 |
+
const content = await readFile(join(extractionRoot, file), 'utf8');
|
| 218 |
+
const match = forbidden.find((value) => content.includes(value));
|
| 219 |
+
if (match === undefined) continue;
|
| 220 |
+
const label = secretValues.includes(match) ? 'a marketplace token' : 'a local filesystem path';
|
| 221 |
+
throw new Error(`Packaged text file ${file} contains ${label}.`);
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
async function verifyRuntimeImports(extensionDir, files) {
|
| 226 |
+
const distFiles = files.filter(
|
| 227 |
+
(file) => file.startsWith('extension/dist/') && ['.cjs', '.js', '.mjs'].includes(extname(file)),
|
| 228 |
+
);
|
| 229 |
+
if (distFiles.length === 0) throw new Error('No JavaScript extension bundle files were packaged.');
|
| 230 |
+
|
| 231 |
+
for (const archivePath of distFiles) {
|
| 232 |
+
const localPath = join(dirname(extensionDir), archivePath);
|
| 233 |
+
const source = await readFile(localPath, 'utf8');
|
| 234 |
+
for (const specifier of collectLiteralImports(source)) {
|
| 235 |
+
if (specifier === 'vscode' || BUILTIN_IMPORTS.has(specifier)) continue;
|
| 236 |
+
if (OPTIONAL_FALLBACK_IMPORTS.has(specifier)) continue;
|
| 237 |
+
if (specifier.startsWith('node:')) continue;
|
| 238 |
+
if (specifier.startsWith('.') || specifier.startsWith('/')) {
|
| 239 |
+
if (specifier.startsWith('/')) {
|
| 240 |
+
throw new Error(`Absolute runtime import "${specifier}" in ${archivePath}.`);
|
| 241 |
+
}
|
| 242 |
+
const dependencyPath = resolve(dirname(localPath), stripImportSuffix(specifier));
|
| 243 |
+
if (!runtimeImportExists(dependencyPath)) {
|
| 244 |
+
throw new Error(`Missing relative runtime import "${specifier}" in ${archivePath}.`);
|
| 245 |
+
}
|
| 246 |
+
continue;
|
| 247 |
+
}
|
| 248 |
+
if (specifier.startsWith('data:') || specifier.startsWith('file:')) continue;
|
| 249 |
+
throw new Error(`Bare runtime dependency "${specifier}" remains in ${archivePath}.`);
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function collectLiteralImports(source) {
|
| 255 |
+
const imports = new Set();
|
| 256 |
+
const program = parse(source, {
|
| 257 |
+
allowHashBang: true,
|
| 258 |
+
ecmaVersion: 'latest',
|
| 259 |
+
sourceType: 'module',
|
| 260 |
+
});
|
| 261 |
+
walkSyntax(program, (node) => {
|
| 262 |
+
if (
|
| 263 |
+
node.type === 'ImportDeclaration' ||
|
| 264 |
+
node.type === 'ExportAllDeclaration' ||
|
| 265 |
+
node.type === 'ExportNamedDeclaration'
|
| 266 |
+
) {
|
| 267 |
+
const specifier = literalString(node.source);
|
| 268 |
+
if (specifier !== undefined) imports.add(specifier);
|
| 269 |
+
return;
|
| 270 |
+
}
|
| 271 |
+
if (node.type === 'ImportExpression') {
|
| 272 |
+
const specifier = literalString(node.source);
|
| 273 |
+
if (specifier !== undefined) imports.add(specifier);
|
| 274 |
+
return;
|
| 275 |
+
}
|
| 276 |
+
if (node.type === 'CallExpression' && isRuntimeRequire(node.callee)) {
|
| 277 |
+
const specifier = literalString(node.arguments?.[0]);
|
| 278 |
+
if (specifier !== undefined) imports.add(specifier);
|
| 279 |
+
}
|
| 280 |
+
});
|
| 281 |
+
return imports;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
function walkSyntax(value, visit) {
|
| 285 |
+
if (Array.isArray(value)) {
|
| 286 |
+
for (const item of value) walkSyntax(item, visit);
|
| 287 |
+
return;
|
| 288 |
+
}
|
| 289 |
+
if (typeof value !== 'object' || value === null) return;
|
| 290 |
+
if (typeof value.type === 'string') visit(value);
|
| 291 |
+
for (const [key, child] of Object.entries(value)) {
|
| 292 |
+
if (key === 'start' || key === 'end' || key === 'loc' || key === 'range') continue;
|
| 293 |
+
walkSyntax(child, visit);
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
function isRuntimeRequire(callee) {
|
| 298 |
+
if (callee?.type === 'Identifier') return /^(?:__)?require\d*$/.test(callee.name);
|
| 299 |
+
if (callee?.type !== 'MemberExpression' || callee.computed === true) return false;
|
| 300 |
+
// `this.require(...)` is an ordinary class method call (e.g. a private field
|
| 301 |
+
// accessor in bundled sources), never a CommonJS require of a bare specifier.
|
| 302 |
+
if (callee.object?.type === 'ThisExpression') return false;
|
| 303 |
+
return callee.property?.type === 'Identifier' && callee.property.name === 'require';
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function literalString(node) {
|
| 307 |
+
if (node?.type === 'Literal' && typeof node.value === 'string') return node.value;
|
| 308 |
+
if (node?.type === 'TemplateLiteral' && node.expressions?.length === 0) {
|
| 309 |
+
return node.quasis?.[0]?.value?.cooked;
|
| 310 |
+
}
|
| 311 |
+
return undefined;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
async function verifyEntryImport(extensionDir, main) {
|
| 315 |
+
const mainPath = join(extensionDir, stripLeadingDotSlash(main));
|
| 316 |
+
const stubDir = join(extensionDir, 'node_modules', 'vscode');
|
| 317 |
+
await mkdir(stubDir, { recursive: true });
|
| 318 |
+
await writeFile(
|
| 319 |
+
join(stubDir, 'package.json'),
|
| 320 |
+
`${JSON.stringify({ name: 'vscode', version: '0.0.0-test', type: 'module', exports: './index.js' }, null, 2)}\n`,
|
| 321 |
+
);
|
| 322 |
+
await writeFile(join(stubDir, 'index.js'), 'export {};\n');
|
| 323 |
+
|
| 324 |
+
const script = [
|
| 325 |
+
`const extension = await import(${JSON.stringify(pathToFileURL(mainPath).href)});`,
|
| 326 |
+
'if (typeof extension.activate !== "function") {',
|
| 327 |
+
' throw new Error("extension bundle does not export activate");',
|
| 328 |
+
'}',
|
| 329 |
+
].join('\n');
|
| 330 |
+
const env = { ...process.env };
|
| 331 |
+
delete env.NODE_PATH;
|
| 332 |
+
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
|
| 333 |
+
cwd: dirname(extensionDir),
|
| 334 |
+
env,
|
| 335 |
+
encoding: 'utf8',
|
| 336 |
+
timeout: 30_000,
|
| 337 |
+
});
|
| 338 |
+
await rm(join(extensionDir, 'node_modules'), { recursive: true, force: true });
|
| 339 |
+
if (result.error !== undefined) {
|
| 340 |
+
throw new Error(`Unable to import the unpacked extension entry: ${result.error.message}`);
|
| 341 |
+
}
|
| 342 |
+
if (result.status !== 0) {
|
| 343 |
+
const detail = conciseProcessError(result.stderr || result.stdout);
|
| 344 |
+
throw new Error(`Unpacked extension entry import failed: ${detail}`);
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
async function readJson(path, label) {
|
| 349 |
+
try {
|
| 350 |
+
return JSON.parse(await readFile(path, 'utf8'));
|
| 351 |
+
} catch (error) {
|
| 352 |
+
throw new Error(`${label} is not valid JSON: ${describeError(error)}`, { cause: error });
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
async function listFiles(root) {
|
| 357 |
+
const output = [];
|
| 358 |
+
async function visit(directory) {
|
| 359 |
+
const entries = await readdir(directory, { withFileTypes: true });
|
| 360 |
+
for (const entry of entries) {
|
| 361 |
+
const localPath = join(directory, entry.name);
|
| 362 |
+
if (entry.isDirectory()) {
|
| 363 |
+
await visit(localPath);
|
| 364 |
+
} else if (entry.isFile()) {
|
| 365 |
+
output.push(relative(root, localPath).split(sep).join('/'));
|
| 366 |
+
} else {
|
| 367 |
+
throw new Error(`Unsupported non-file entry in unpacked VSIX: ${localPath}`);
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
}
|
| 371 |
+
await visit(root);
|
| 372 |
+
return output.sort();
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
async function totalSize(root, files) {
|
| 376 |
+
let bytes = 0;
|
| 377 |
+
for (const file of files) bytes += (await stat(join(root, file))).size;
|
| 378 |
+
return bytes;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
function requireFile(fileSet, file) {
|
| 382 |
+
if (fileSet.has(file)) return;
|
| 383 |
+
throw new Error(`Required VSIX resource is missing: ${file}`);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
function requireOneOf(fileSet, files, label) {
|
| 387 |
+
if (files.some((file) => fileSet.has(file))) return;
|
| 388 |
+
throw new Error(`Required VSIX resource is missing: ${label} (${files.join(' or ')}).`);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
function runtimeImportExists(path) {
|
| 392 |
+
return [path, `${path}.js`, `${path}.mjs`, `${path}.cjs`, join(path, 'index.js')].some(existsSync);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
function stripImportSuffix(specifier) {
|
| 396 |
+
return specifier.split(/[?#]/, 1)[0];
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
function stripLeadingDotSlash(value) {
|
| 400 |
+
return String(value).replace(/^\.\//, '');
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
function describeError(error) {
|
| 404 |
+
return error instanceof Error ? error.message : String(error);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
function conciseProcessError(output) {
|
| 408 |
+
const lines = String(output).trim().split(/\r?\n/).filter(Boolean);
|
| 409 |
+
return lines.slice(-4).join('\n') || 'process exited without an error message';
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function parseArguments(argv) {
|
| 413 |
+
const targets = [];
|
| 414 |
+
let outputDir = defaultVsixOutputDir;
|
| 415 |
+
let file;
|
| 416 |
+
let directory;
|
| 417 |
+
let help = false;
|
| 418 |
+
|
| 419 |
+
for (let index = 0; index < argv.length; index += 1) {
|
| 420 |
+
const argument = argv[index];
|
| 421 |
+
if (argument === '--') {
|
| 422 |
+
continue;
|
| 423 |
+
} else if (argument === '--help' || argument === '-h') {
|
| 424 |
+
help = true;
|
| 425 |
+
} else if (argument === '--out-dir') {
|
| 426 |
+
outputDir = requireOptionValue(argv, ++index, '--out-dir');
|
| 427 |
+
} else if (argument === '--target') {
|
| 428 |
+
targets.push(requireOptionValue(argv, ++index, '--target'));
|
| 429 |
+
} else if (argument === '--file') {
|
| 430 |
+
file = requireOptionValue(argv, ++index, '--file');
|
| 431 |
+
} else if (argument === '--directory') {
|
| 432 |
+
directory = requireOptionValue(argv, ++index, '--directory');
|
| 433 |
+
} else if (argument.startsWith('-')) {
|
| 434 |
+
throw new Error(`Unknown option: ${argument}`);
|
| 435 |
+
} else {
|
| 436 |
+
targets.push(argument);
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
if (file !== undefined && directory !== undefined) {
|
| 441 |
+
throw new Error('--file and --directory cannot be used together.');
|
| 442 |
+
}
|
| 443 |
+
const normalizedTargets = normalizeVsixTargets(targets);
|
| 444 |
+
if ((file !== undefined || directory !== undefined) && normalizedTargets.length !== 1) {
|
| 445 |
+
throw new Error('--file and --directory require exactly one target.');
|
| 446 |
+
}
|
| 447 |
+
return { targets: normalizedTargets, outputDir: resolve(outputDir), file, directory, help };
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
function requireOptionValue(argv, index, option) {
|
| 451 |
+
const value = argv[index];
|
| 452 |
+
if (value !== undefined && !value.startsWith('-')) return value;
|
| 453 |
+
throw new Error(`${option} requires a value.`);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
function usage() {
|
| 457 |
+
return [
|
| 458 |
+
'Usage: node scripts/vsix-verify.mjs [targets...] [--out-dir <directory>]',
|
| 459 |
+
' node scripts/vsix-verify.mjs --target <target> --file <file.vsix>',
|
| 460 |
+
' node scripts/vsix-verify.mjs --target <target> --directory <unpacked-vsix>',
|
| 461 |
+
'',
|
| 462 |
+
'The verifier performs a package-content audit and an entry import smoke only.',
|
| 463 |
+
'It does not claim that a target passed a real operating-system E2E run.',
|
| 464 |
+
].join('\n');
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
async function main() {
|
| 468 |
+
const options = parseArguments(process.argv.slice(2));
|
| 469 |
+
if (options.help) {
|
| 470 |
+
console.log(usage());
|
| 471 |
+
return;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
for (const target of options.targets) {
|
| 475 |
+
const input = options.directory ?? options.file ?? join(options.outputDir, vsixFileName(target));
|
| 476 |
+
const result = options.directory === undefined
|
| 477 |
+
? await verifyVsix(resolve(input), target, { sourceRoot: extensionRoot })
|
| 478 |
+
: await auditExtractedVsix(resolve(input), target, { sourceRoot: extensionRoot });
|
| 479 |
+
console.log(
|
| 480 |
+
`Verified ${target}: ${result.files} files, ${result.bytes} unpacked bytes; static audit and entry import smoke passed (package-only).`,
|
| 481 |
+
);
|
| 482 |
+
}
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
if (isMainModule(import.meta.url)) {
|
| 486 |
+
main().catch((error) => {
|
| 487 |
+
console.error(`VSIX verification failed: ${describeError(error)}`);
|
| 488 |
+
process.exitCode = 1;
|
| 489 |
+
});
|
| 490 |
+
}
|
apps/vscode/scripts/watch-extension.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
import { existsSync } from 'node:fs';
|
| 3 |
+
import { readdir, stat } from 'node:fs/promises';
|
| 4 |
+
import { extname, join, resolve } from 'node:path';
|
| 5 |
+
|
| 6 |
+
import { runLocalCli } from './local-cli.mjs';
|
| 7 |
+
import { extensionRoot, isMainModule } from './vsix-targets.mjs';
|
| 8 |
+
|
| 9 |
+
const sourceDirectories = [
|
| 10 |
+
join(extensionRoot, 'src'),
|
| 11 |
+
join(extensionRoot, 'shared'),
|
| 12 |
+
...[
|
| 13 |
+
'kaos',
|
| 14 |
+
'kosong',
|
| 15 |
+
'migration-legacy',
|
| 16 |
+
'node-sdk',
|
| 17 |
+
'oauth',
|
| 18 |
+
'protocol',
|
| 19 |
+
'telemetry',
|
| 20 |
+
].map((name) => resolve(extensionRoot, `../../packages/${name}/src`)),
|
| 21 |
+
].filter(existsSync);
|
| 22 |
+
const rootConfigFiles = ['package.json', 'tsconfig.json', 'tsdown.config.ts'].map((name) =>
|
| 23 |
+
join(extensionRoot, name),
|
| 24 |
+
);
|
| 25 |
+
const watchedExtensions = new Set(['.cts', '.js', '.json', '.md', '.mjs', '.mts', '.ts', '.tsx']);
|
| 26 |
+
const pollIntervalMs = 1_500;
|
| 27 |
+
|
| 28 |
+
function buildExtension() {
|
| 29 |
+
runLocalCli(
|
| 30 |
+
'tsdown',
|
| 31 |
+
'tsdown',
|
| 32 |
+
['--config', join(extensionRoot, 'tsdown.config.ts'), '--sourcemap'],
|
| 33 |
+
{ cwd: extensionRoot },
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async function main() {
|
| 38 |
+
buildExtension();
|
| 39 |
+
let snapshot = await sourceSnapshot();
|
| 40 |
+
let checking = false;
|
| 41 |
+
const poll = async () => {
|
| 42 |
+
if (checking) return;
|
| 43 |
+
checking = true;
|
| 44 |
+
try {
|
| 45 |
+
const nextSnapshot = await sourceSnapshot();
|
| 46 |
+
if (nextSnapshot === snapshot) return;
|
| 47 |
+
snapshot = nextSnapshot;
|
| 48 |
+
console.log('\nExtension source changed; rebuilding with sourcemaps...');
|
| 49 |
+
buildExtension();
|
| 50 |
+
console.log('Extension rebuild complete.');
|
| 51 |
+
} catch (error) {
|
| 52 |
+
console.error(`Extension watch check failed: ${error instanceof Error ? error.message : String(error)}`);
|
| 53 |
+
} finally {
|
| 54 |
+
checking = false;
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
| 58 |
+
|
| 59 |
+
const close = () => {
|
| 60 |
+
clearInterval(interval);
|
| 61 |
+
process.exit(0);
|
| 62 |
+
};
|
| 63 |
+
process.once('SIGINT', close);
|
| 64 |
+
process.once('SIGTERM', close);
|
| 65 |
+
console.log('Extension dev build complete; watching for changes.');
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
async function sourceSnapshot() {
|
| 69 |
+
const records = [];
|
| 70 |
+
for (const directory of sourceDirectories) await collectSourceRecords(directory, records);
|
| 71 |
+
for (const file of rootConfigFiles) await addFileRecord(file, records);
|
| 72 |
+
return records.sort().join('\n');
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
async function collectSourceRecords(directory, records) {
|
| 76 |
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
| 77 |
+
const path = join(directory, entry.name);
|
| 78 |
+
if (entry.isDirectory()) {
|
| 79 |
+
await collectSourceRecords(path, records);
|
| 80 |
+
} else if (entry.isFile() && watchedExtensions.has(extname(entry.name))) {
|
| 81 |
+
await addFileRecord(path, records);
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
async function addFileRecord(path, records) {
|
| 87 |
+
const info = await stat(path);
|
| 88 |
+
records.push(`${path}:${info.size}:${info.mtimeMs}`);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if (isMainModule(import.meta.url)) {
|
| 92 |
+
main().catch((error) => {
|
| 93 |
+
console.error(`Extension watch failed: ${error instanceof Error ? error.message : String(error)}`);
|
| 94 |
+
process.exitCode = 1;
|
| 95 |
+
});
|
| 96 |
+
}
|
apps/vscode/scripts/zip.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
| 2 |
+
import { dirname, resolve, sep } from 'node:path';
|
| 3 |
+
import { inflateRawSync } from 'node:zlib';
|
| 4 |
+
|
| 5 |
+
const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
|
| 6 |
+
const CENTRAL_DIRECTORY_ENTRY = 0x02014b50;
|
| 7 |
+
const LOCAL_FILE_HEADER = 0x04034b50;
|
| 8 |
+
const MAX_END_RECORD_SEARCH = 65_535 + 22;
|
| 9 |
+
|
| 10 |
+
export async function extractZip(archivePath, destination) {
|
| 11 |
+
const archive = await readFile(archivePath);
|
| 12 |
+
const entries = readCentralDirectory(archive);
|
| 13 |
+
|
| 14 |
+
for (const entry of entries) {
|
| 15 |
+
const relativePath = safeArchivePath(entry.name);
|
| 16 |
+
if (relativePath === '') continue;
|
| 17 |
+
const outputPath = resolve(destination, relativePath);
|
| 18 |
+
assertInside(destination, outputPath, entry.name);
|
| 19 |
+
|
| 20 |
+
if (entry.name.endsWith('/')) {
|
| 21 |
+
await mkdir(outputPath, { recursive: true });
|
| 22 |
+
continue;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const content = readEntry(archive, entry);
|
| 26 |
+
await mkdir(dirname(outputPath), { recursive: true });
|
| 27 |
+
await writeFile(outputPath, content);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return entries.map((entry) => entry.name);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function readCentralDirectory(archive) {
|
| 34 |
+
const endOffset = findEndRecord(archive);
|
| 35 |
+
const entryCount = archive.readUInt16LE(endOffset + 10);
|
| 36 |
+
const directorySize = archive.readUInt32LE(endOffset + 12);
|
| 37 |
+
const directoryOffset = archive.readUInt32LE(endOffset + 16);
|
| 38 |
+
|
| 39 |
+
if (entryCount === 0xffff || directorySize === 0xffffffff || directoryOffset === 0xffffffff) {
|
| 40 |
+
throw new Error('ZIP64 VSIX archives are not supported by the verifier.');
|
| 41 |
+
}
|
| 42 |
+
if (directoryOffset + directorySize > archive.length) {
|
| 43 |
+
throw new Error('VSIX central directory points outside the archive.');
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const entries = [];
|
| 47 |
+
let offset = directoryOffset;
|
| 48 |
+
for (let index = 0; index < entryCount; index += 1) {
|
| 49 |
+
if (archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_ENTRY) {
|
| 50 |
+
throw new Error(`Invalid VSIX central-directory entry at byte ${offset}.`);
|
| 51 |
+
}
|
| 52 |
+
const flags = archive.readUInt16LE(offset + 8);
|
| 53 |
+
if ((flags & 0x1) !== 0) throw new Error('Encrypted VSIX entries are not supported.');
|
| 54 |
+
|
| 55 |
+
const compression = archive.readUInt16LE(offset + 10);
|
| 56 |
+
const compressedSize = archive.readUInt32LE(offset + 20);
|
| 57 |
+
const uncompressedSize = archive.readUInt32LE(offset + 24);
|
| 58 |
+
const nameLength = archive.readUInt16LE(offset + 28);
|
| 59 |
+
const extraLength = archive.readUInt16LE(offset + 30);
|
| 60 |
+
const commentLength = archive.readUInt16LE(offset + 32);
|
| 61 |
+
const localOffset = archive.readUInt32LE(offset + 42);
|
| 62 |
+
const nameStart = offset + 46;
|
| 63 |
+
const name = archive.subarray(nameStart, nameStart + nameLength).toString('utf8');
|
| 64 |
+
entries.push({ name, compression, compressedSize, uncompressedSize, localOffset });
|
| 65 |
+
offset = nameStart + nameLength + extraLength + commentLength;
|
| 66 |
+
}
|
| 67 |
+
return entries;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function findEndRecord(archive) {
|
| 71 |
+
const lowerBound = Math.max(0, archive.length - MAX_END_RECORD_SEARCH);
|
| 72 |
+
for (let offset = archive.length - 22; offset >= lowerBound; offset -= 1) {
|
| 73 |
+
if (archive.readUInt32LE(offset) === END_OF_CENTRAL_DIRECTORY) return offset;
|
| 74 |
+
}
|
| 75 |
+
throw new Error('VSIX is not a readable ZIP archive: end record is missing.');
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function readEntry(archive, entry) {
|
| 79 |
+
if (archive.readUInt32LE(entry.localOffset) !== LOCAL_FILE_HEADER) {
|
| 80 |
+
throw new Error(`Invalid local header for VSIX entry ${entry.name}.`);
|
| 81 |
+
}
|
| 82 |
+
const nameLength = archive.readUInt16LE(entry.localOffset + 26);
|
| 83 |
+
const extraLength = archive.readUInt16LE(entry.localOffset + 28);
|
| 84 |
+
const dataStart = entry.localOffset + 30 + nameLength + extraLength;
|
| 85 |
+
const dataEnd = dataStart + entry.compressedSize;
|
| 86 |
+
if (dataEnd > archive.length) throw new Error(`Truncated VSIX entry ${entry.name}.`);
|
| 87 |
+
|
| 88 |
+
const compressed = archive.subarray(dataStart, dataEnd);
|
| 89 |
+
let content;
|
| 90 |
+
if (entry.compression === 0) {
|
| 91 |
+
content = compressed;
|
| 92 |
+
} else if (entry.compression === 8) {
|
| 93 |
+
content = inflateRawSync(compressed);
|
| 94 |
+
} else {
|
| 95 |
+
throw new Error(`Unsupported compression method ${entry.compression} for ${entry.name}.`);
|
| 96 |
+
}
|
| 97 |
+
if (content.length !== entry.uncompressedSize) {
|
| 98 |
+
throw new Error(
|
| 99 |
+
`Unexpected uncompressed size for ${entry.name}: ${content.length}, expected ${entry.uncompressedSize}.`,
|
| 100 |
+
);
|
| 101 |
+
}
|
| 102 |
+
return content;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function safeArchivePath(name) {
|
| 106 |
+
const normalized = name.replaceAll('\\', '/');
|
| 107 |
+
const segments = normalized.split('/').filter((segment) => segment !== '');
|
| 108 |
+
if (
|
| 109 |
+
normalized.startsWith('/') ||
|
| 110 |
+
/^[A-Za-z]:/.test(normalized) ||
|
| 111 |
+
segments.some((segment) => segment === '..')
|
| 112 |
+
) {
|
| 113 |
+
throw new Error(`Unsafe path in VSIX archive: ${name}`);
|
| 114 |
+
}
|
| 115 |
+
return segments.join(sep);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
function assertInside(destination, outputPath, archiveName) {
|
| 119 |
+
const root = resolve(destination);
|
| 120 |
+
if (outputPath === root || outputPath.startsWith(`${root}${sep}`)) return;
|
| 121 |
+
throw new Error(`Unsafe path in VSIX archive: ${archiveName}`);
|
| 122 |
+
}
|
apps/vscode/shared/bridge.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Bridge Protocol - Communication between VS Code extension and webview.
|
| 3 |
+
*
|
| 4 |
+
* Architecture:
|
| 5 |
+
* - Webview calls Methods via RPC (request/response)
|
| 6 |
+
* - Extension broadcasts Events to webview (one-way notifications)
|
| 7 |
+
*
|
| 8 |
+
* RPC flow: webview.call(method, params) -> extension.dispatch -> webview.resolve(result)
|
| 9 |
+
* Event flow: extension.broadcast(event, data) -> webview.on(event, handler)
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
export const Methods = {
|
| 13 |
+
CheckWorkspace: "checkWorkspace",
|
| 14 |
+
GetInputHistory: "getInputHistory",
|
| 15 |
+
AddInputHistory: "addInputHistory",
|
| 16 |
+
|
| 17 |
+
GetSlashCommands: "getSlashCommands",
|
| 18 |
+
CheckLoginStatus: "checkLoginStatus",
|
| 19 |
+
Login: "login",
|
| 20 |
+
Logout: "logout",
|
| 21 |
+
SaveConfig: "saveConfig",
|
| 22 |
+
GetExtensionConfig: "getExtensionConfig",
|
| 23 |
+
OpenSettings: "openSettings",
|
| 24 |
+
OpenFolder: "openFolder",
|
| 25 |
+
GetModels: "getModels",
|
| 26 |
+
|
| 27 |
+
GetMCPServers: "getMCPServers",
|
| 28 |
+
AddMCPServer: "addMCPServer",
|
| 29 |
+
UpdateMCPServer: "updateMCPServer",
|
| 30 |
+
RemoveMCPServer: "removeMCPServer",
|
| 31 |
+
AuthMCP: "authMCP",
|
| 32 |
+
ResetAuthMCP: "resetAuthMCP",
|
| 33 |
+
TestMCP: "testMCP",
|
| 34 |
+
|
| 35 |
+
StreamChat: "streamChat",
|
| 36 |
+
AbortChat: "abortChat",
|
| 37 |
+
ResetSession: "resetSession",
|
| 38 |
+
SetPlanMode: "setPlanMode",
|
| 39 |
+
SteerChat: "steerChat",
|
| 40 |
+
RespondApproval: "respondApproval",
|
| 41 |
+
|
| 42 |
+
GetKimiSessions: "getKimiSessions",
|
| 43 |
+
GetAllKimiSessions: "getAllKimiSessions",
|
| 44 |
+
GetRegisteredWorkDirs: "getRegisteredWorkDirs",
|
| 45 |
+
SetWorkDir: "setWorkDir",
|
| 46 |
+
BrowseWorkDir: "browseWorkDir",
|
| 47 |
+
LoadKimiSessionHistory: "loadKimiSessionHistory",
|
| 48 |
+
DeleteKimiSession: "deleteKimiSession",
|
| 49 |
+
ForkKimiSession: "forkKimiSession",
|
| 50 |
+
GetProjectFiles: "getProjectFiles",
|
| 51 |
+
PickMedia: "pickMedia",
|
| 52 |
+
OpenFile: "openFile",
|
| 53 |
+
CheckFileExists: "checkFileExists",
|
| 54 |
+
CheckFilesExist: "checkFilesExist",
|
| 55 |
+
OpenFileDiff: "openFileDiff",
|
| 56 |
+
TrackFiles: "trackFiles",
|
| 57 |
+
ClearTrackedFiles: "clearTrackedFiles",
|
| 58 |
+
RevertFiles: "revertFiles",
|
| 59 |
+
KeepChanges: "keepChanges",
|
| 60 |
+
GetImageDataUri: "getImageDataUri",
|
| 61 |
+
ShowLogs: "showLogs",
|
| 62 |
+
ReloadWebview: "reloadWebview",
|
| 63 |
+
RespondQuestion: "respondQuestion",
|
| 64 |
+
} as const;
|
| 65 |
+
|
| 66 |
+
export type RpcMethod = (typeof Methods)[keyof typeof Methods];
|
| 67 |
+
|
| 68 |
+
export interface RpcMessage {
|
| 69 |
+
readonly id: string;
|
| 70 |
+
readonly method: RpcMethod;
|
| 71 |
+
readonly params?: unknown;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
export interface RpcResult {
|
| 75 |
+
readonly id: string;
|
| 76 |
+
readonly result?: unknown;
|
| 77 |
+
readonly error?: string;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export type RpcMessageValidation =
|
| 81 |
+
| { readonly ok: true; readonly message: RpcMessage }
|
| 82 |
+
| {
|
| 83 |
+
readonly ok: false;
|
| 84 |
+
readonly id: string;
|
| 85 |
+
readonly method: string;
|
| 86 |
+
readonly error: string;
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
export const Events = {
|
| 90 |
+
ExtensionConfigChanged: "extensionConfigChanged",
|
| 91 |
+
MCPServersChanged: "mcpServersChanged",
|
| 92 |
+
StreamEvent: "streamEvent",
|
| 93 |
+
FocusInput: "focusInput",
|
| 94 |
+
InsertMention: "insertMention",
|
| 95 |
+
NewConversation: "newConversation",
|
| 96 |
+
FileChangesUpdated: "fileChangesUpdated",
|
| 97 |
+
RollbackInput: "rollbackInput",
|
| 98 |
+
LoginUrl: "loginUrl",
|
| 99 |
+
} as const;
|
| 100 |
+
|
| 101 |
+
const rpcMethods = new Set<string>(Object.values(Methods));
|
| 102 |
+
|
| 103 |
+
/** Validates the untrusted Webview message before any host-side handler runs. */
|
| 104 |
+
export function validateRpcMessage(value: unknown): RpcMessageValidation {
|
| 105 |
+
if (!isPlainObject(value)) {
|
| 106 |
+
return invalidMessage("", "<invalid>", "Invalid bridge request: expected a plain object.");
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
const id = value["id"];
|
| 110 |
+
if (!Object.hasOwn(value, "id") || typeof id !== "string" || id.trim().length === 0) {
|
| 111 |
+
return invalidMessage("", safeMethod(value["method"]), "Invalid bridge request: id must be a non-empty string.");
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
const method = value["method"];
|
| 115 |
+
if (!Object.hasOwn(value, "method") || typeof method !== "string" || method.trim().length === 0) {
|
| 116 |
+
return invalidMessage(id, "<invalid>", "Invalid bridge request: method must be a non-empty string.");
|
| 117 |
+
}
|
| 118 |
+
if (!rpcMethods.has(method)) {
|
| 119 |
+
return invalidMessage(id, method, `Unknown bridge method: ${method}`);
|
| 120 |
+
}
|
| 121 |
+
const params = Object.hasOwn(value, "params") ? value["params"] : undefined;
|
| 122 |
+
if (!validateParams(method as RpcMethod, params)) {
|
| 123 |
+
return invalidMessage(id, method, `Invalid bridge params for method: ${method}`);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
return { ok: true, message: { id, method: method as RpcMethod, params } };
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
function validateParams(method: RpcMethod, params: unknown): boolean {
|
| 130 |
+
switch (method) {
|
| 131 |
+
case Methods.CheckWorkspace:
|
| 132 |
+
case Methods.GetInputHistory:
|
| 133 |
+
case Methods.GetSlashCommands:
|
| 134 |
+
case Methods.CheckLoginStatus:
|
| 135 |
+
case Methods.Login:
|
| 136 |
+
case Methods.Logout:
|
| 137 |
+
case Methods.GetExtensionConfig:
|
| 138 |
+
case Methods.OpenSettings:
|
| 139 |
+
case Methods.OpenFolder:
|
| 140 |
+
case Methods.GetModels:
|
| 141 |
+
case Methods.GetMCPServers:
|
| 142 |
+
case Methods.AbortChat:
|
| 143 |
+
case Methods.ResetSession:
|
| 144 |
+
case Methods.GetKimiSessions:
|
| 145 |
+
case Methods.GetAllKimiSessions:
|
| 146 |
+
case Methods.GetRegisteredWorkDirs:
|
| 147 |
+
case Methods.BrowseWorkDir:
|
| 148 |
+
case Methods.ClearTrackedFiles:
|
| 149 |
+
case Methods.ShowLogs:
|
| 150 |
+
case Methods.ReloadWebview:
|
| 151 |
+
return params === undefined;
|
| 152 |
+
|
| 153 |
+
case Methods.AddInputHistory:
|
| 154 |
+
return hasString(params, "text");
|
| 155 |
+
case Methods.SaveConfig:
|
| 156 |
+
return isPlainObject(params)
|
| 157 |
+
&& typeof params["model"] === "string"
|
| 158 |
+
&& isOptionalType(params["thinking"], "boolean")
|
| 159 |
+
&& isOptionalType(params["effort"], "string")
|
| 160 |
+
&& isOptionalType(params["effortChanged"], "boolean");
|
| 161 |
+
case Methods.AddMCPServer:
|
| 162 |
+
return isMcpServerConfig(params);
|
| 163 |
+
case Methods.UpdateMCPServer:
|
| 164 |
+
return isMcpUpdate(params);
|
| 165 |
+
case Methods.RemoveMCPServer:
|
| 166 |
+
case Methods.AuthMCP:
|
| 167 |
+
case Methods.ResetAuthMCP:
|
| 168 |
+
case Methods.TestMCP:
|
| 169 |
+
return hasNonEmptyString(params, "name");
|
| 170 |
+
case Methods.StreamChat:
|
| 171 |
+
return isStreamChatParams(params);
|
| 172 |
+
case Methods.RespondApproval:
|
| 173 |
+
return isPlainObject(params)
|
| 174 |
+
&& isNonEmptyString(params["requestId"])
|
| 175 |
+
&& (params["response"] === "approve"
|
| 176 |
+
|| params["response"] === "approve_for_session"
|
| 177 |
+
|| params["response"] === "reject");
|
| 178 |
+
case Methods.RespondQuestion:
|
| 179 |
+
return isPlainObject(params)
|
| 180 |
+
&& isNonEmptyString(params["rpcRequestId"])
|
| 181 |
+
&& isNonEmptyString(params["questionRequestId"])
|
| 182 |
+
&& isStringRecord(params["answers"]);
|
| 183 |
+
case Methods.SetPlanMode:
|
| 184 |
+
return hasBoolean(params, "enabled");
|
| 185 |
+
case Methods.SteerChat:
|
| 186 |
+
return isPlainObject(params) && isContent(params["content"]);
|
| 187 |
+
case Methods.GetProjectFiles:
|
| 188 |
+
return params === undefined || (
|
| 189 |
+
isPlainObject(params)
|
| 190 |
+
&& isOptionalType(params["query"], "string")
|
| 191 |
+
&& isOptionalType(params["directory"], "string")
|
| 192 |
+
);
|
| 193 |
+
case Methods.SetWorkDir:
|
| 194 |
+
return isPlainObject(params) && (params["workDir"] === null || typeof params["workDir"] === "string");
|
| 195 |
+
case Methods.LoadKimiSessionHistory:
|
| 196 |
+
return hasNonEmptyString(params, "kimiSessionId");
|
| 197 |
+
case Methods.DeleteKimiSession:
|
| 198 |
+
return hasNonEmptyString(params, "sessionId");
|
| 199 |
+
case Methods.ForkKimiSession:
|
| 200 |
+
return isPlainObject(params)
|
| 201 |
+
&& isNonEmptyString(params["sessionId"])
|
| 202 |
+
&& Number.isInteger(params["turnIndex"])
|
| 203 |
+
&& (params["turnIndex"] as number) >= 0;
|
| 204 |
+
case Methods.PickMedia:
|
| 205 |
+
return isPlainObject(params)
|
| 206 |
+
&& (params["maxCount"] === undefined
|
| 207 |
+
|| (Number.isInteger(params["maxCount"]) && (params["maxCount"] as number) >= 0))
|
| 208 |
+
&& isOptionalType(params["includeVideo"], "boolean");
|
| 209 |
+
case Methods.OpenFile:
|
| 210 |
+
case Methods.OpenFileDiff:
|
| 211 |
+
case Methods.CheckFileExists:
|
| 212 |
+
case Methods.GetImageDataUri:
|
| 213 |
+
return hasString(params, "filePath");
|
| 214 |
+
case Methods.CheckFilesExist:
|
| 215 |
+
case Methods.TrackFiles:
|
| 216 |
+
return hasStringArray(params, "paths");
|
| 217 |
+
case Methods.RevertFiles:
|
| 218 |
+
case Methods.KeepChanges:
|
| 219 |
+
return isPlainObject(params) && isOptionalType(params["filePath"], "string");
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function isStreamChatParams(value: unknown): boolean {
|
| 224 |
+
return isPlainObject(value)
|
| 225 |
+
&& isContent(value["content"])
|
| 226 |
+
&& typeof value["model"] === "string"
|
| 227 |
+
&& isOptionalType(value["effort"], "string")
|
| 228 |
+
&& isOptionalType(value["thinking"], "boolean")
|
| 229 |
+
&& isOptionalType(value["planMode"], "boolean")
|
| 230 |
+
&& isOptionalType(value["sessionId"], "string");
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
function isContent(value: unknown): boolean {
|
| 234 |
+
return typeof value === "string" || (Array.isArray(value) && value.every(isContentPart));
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
function isContentPart(value: unknown): boolean {
|
| 238 |
+
if (!isPlainObject(value) || typeof value["type"] !== "string") return false;
|
| 239 |
+
switch (value["type"]) {
|
| 240 |
+
case "text":
|
| 241 |
+
return typeof value["text"] === "string";
|
| 242 |
+
case "think":
|
| 243 |
+
return typeof value["think"] === "string"
|
| 244 |
+
&& (value["encrypted"] === undefined
|
| 245 |
+
|| value["encrypted"] === null
|
| 246 |
+
|| typeof value["encrypted"] === "string");
|
| 247 |
+
case "image_url":
|
| 248 |
+
case "audio_url":
|
| 249 |
+
case "video_url": {
|
| 250 |
+
const media = value[value["type"]];
|
| 251 |
+
return isPlainObject(media)
|
| 252 |
+
&& typeof media["url"] === "string"
|
| 253 |
+
&& (media["id"] === undefined || media["id"] === null || typeof media["id"] === "string");
|
| 254 |
+
}
|
| 255 |
+
default:
|
| 256 |
+
return false;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
function isMcpUpdate(value: unknown): boolean {
|
| 261 |
+
if (!isPlainObject(value)) return false;
|
| 262 |
+
if (Object.hasOwn(value, "server")) {
|
| 263 |
+
return isNonEmptyString(value["originalName"]) && isMcpServerConfig(value["server"]);
|
| 264 |
+
}
|
| 265 |
+
return isMcpServerConfig(value);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function isMcpServerConfig(value: unknown): boolean {
|
| 269 |
+
return isPlainObject(value)
|
| 270 |
+
&& isNonEmptyString(value["name"])
|
| 271 |
+
&& (value["transport"] === "stdio" || value["transport"] === "http")
|
| 272 |
+
&& isOptionalType(value["url"], "string")
|
| 273 |
+
&& isOptionalType(value["command"], "string")
|
| 274 |
+
&& (value["args"] === undefined || isStringArray(value["args"]))
|
| 275 |
+
&& (value["env"] === undefined || isStringRecord(value["env"]))
|
| 276 |
+
&& (value["headers"] === undefined || isStringRecord(value["headers"]))
|
| 277 |
+
&& (value["auth"] === undefined || value["auth"] === "oauth")
|
| 278 |
+
&& isOptionalType(value["bearerTokenEnvVar"], "string");
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
function hasString(value: unknown, key: string): boolean {
|
| 282 |
+
return isPlainObject(value) && typeof value[key] === "string";
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function hasNonEmptyString(value: unknown, key: string): boolean {
|
| 286 |
+
return isPlainObject(value) && isNonEmptyString(value[key]);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
function hasBoolean(value: unknown, key: string): boolean {
|
| 290 |
+
return isPlainObject(value) && typeof value[key] === "boolean";
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
function hasStringArray(value: unknown, key: string): boolean {
|
| 294 |
+
return isPlainObject(value) && isStringArray(value[key]);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
function isStringArray(value: unknown): boolean {
|
| 298 |
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
function isStringRecord(value: unknown): boolean {
|
| 302 |
+
return isPlainObject(value) && Object.values(value).every((item) => typeof item === "string");
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
function isOptionalType(value: unknown, type: "string" | "boolean"): boolean {
|
| 306 |
+
return value === undefined || typeof value === type;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
function isNonEmptyString(value: unknown): value is string {
|
| 310 |
+
return typeof value === "string" && value.trim().length > 0;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
| 314 |
+
if (value === null || typeof value !== "object") return false;
|
| 315 |
+
const prototype = Object.getPrototypeOf(value);
|
| 316 |
+
return prototype === Object.prototype || prototype === null;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
function invalidMessage(id: string, method: string, error: string): RpcMessageValidation {
|
| 320 |
+
return { ok: false, id, method, error };
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
function safeMethod(value: unknown): string {
|
| 324 |
+
return typeof value === "string" && value.length > 0 ? value : "<invalid>";
|
| 325 |
+
}
|
apps/vscode/shared/errors.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ErrorPhase } from "./types";
|
| 2 |
+
|
| 3 |
+
// Keep the released Webview's error contract local. The upper-case values are
|
| 4 |
+
// accepted for sessions restored from the legacy extension; the dotted values
|
| 5 |
+
// are emitted by the in-process v1 core.
|
| 6 |
+
const LEGACY = {
|
| 7 |
+
CLI_NOT_FOUND: "CLI_NOT_FOUND",
|
| 8 |
+
SPAWN_FAILED: "SPAWN_FAILED",
|
| 9 |
+
ALREADY_STARTED: "ALREADY_STARTED",
|
| 10 |
+
STDIN_NOT_WRITABLE: "STDIN_NOT_WRITABLE",
|
| 11 |
+
HANDSHAKE_TIMEOUT: "HANDSHAKE_TIMEOUT",
|
| 12 |
+
PROCESS_CRASHED: "PROCESS_CRASHED",
|
| 13 |
+
LLM_NOT_SET: "LLM_NOT_SET",
|
| 14 |
+
LLM_NOT_SUPPORTED: "LLM_NOT_SUPPORTED",
|
| 15 |
+
INVALID_STATE: "INVALID_STATE",
|
| 16 |
+
CHAT_PROVIDER_ERROR: "CHAT_PROVIDER_ERROR",
|
| 17 |
+
SESSION_BUSY: "SESSION_BUSY",
|
| 18 |
+
SESSION_CLOSED: "SESSION_CLOSED",
|
| 19 |
+
TURN_INTERRUPTED: "TURN_INTERRUPTED",
|
| 20 |
+
INVALID_JSON: "INVALID_JSON",
|
| 21 |
+
INVALID_REQUEST: "INVALID_REQUEST",
|
| 22 |
+
INVALID_PARAMS: "INVALID_PARAMS",
|
| 23 |
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
| 24 |
+
} as const;
|
| 25 |
+
|
| 26 |
+
// Pre-flight: task didn't start at all or was blocked by "gatekeeper"
|
| 27 |
+
export const PREFLIGHT_CODES = new Set<string>([
|
| 28 |
+
LEGACY.CLI_NOT_FOUND,
|
| 29 |
+
LEGACY.SPAWN_FAILED,
|
| 30 |
+
LEGACY.ALREADY_STARTED,
|
| 31 |
+
LEGACY.STDIN_NOT_WRITABLE,
|
| 32 |
+
LEGACY.PROCESS_CRASHED,
|
| 33 |
+
LEGACY.LLM_NOT_SET,
|
| 34 |
+
LEGACY.LLM_NOT_SUPPORTED,
|
| 35 |
+
LEGACY.INVALID_STATE,
|
| 36 |
+
LEGACY.SESSION_BUSY,
|
| 37 |
+
"config.invalid",
|
| 38 |
+
"model.not_configured",
|
| 39 |
+
"auth.login_required",
|
| 40 |
+
"session.not_found",
|
| 41 |
+
"session.state_not_found",
|
| 42 |
+
"session.state_invalid",
|
| 43 |
+
"session.init_failed",
|
| 44 |
+
"shell.git_bash_not_found",
|
| 45 |
+
]);
|
| 46 |
+
|
| 47 |
+
// User-friendly error messages
|
| 48 |
+
export const ERROR_MESSAGES: Record<string, string> = {
|
| 49 |
+
// Pre-flight
|
| 50 |
+
[LEGACY.CLI_NOT_FOUND]: "Kimi Code CLI not found.",
|
| 51 |
+
[LEGACY.SPAWN_FAILED]: "Failed to start Kimi Code CLI.",
|
| 52 |
+
[LEGACY.ALREADY_STARTED]: "A session is already running.",
|
| 53 |
+
[LEGACY.STDIN_NOT_WRITABLE]: "Failed to communicate with Kimi Code CLI.",
|
| 54 |
+
[LEGACY.HANDSHAKE_TIMEOUT]: "Connection timed out.",
|
| 55 |
+
[LEGACY.PROCESS_CRASHED]: "Process connection lost.",
|
| 56 |
+
|
| 57 |
+
// CLI errors
|
| 58 |
+
[LEGACY.LLM_NOT_SET]: "Authentication failed. Please sign in.",
|
| 59 |
+
[LEGACY.LLM_NOT_SUPPORTED]: "This model is not supported.",
|
| 60 |
+
[LEGACY.INVALID_STATE]: "Please wait for the current operation.",
|
| 61 |
+
[LEGACY.CHAT_PROVIDER_ERROR]: "Service temporarily unavailable.",
|
| 62 |
+
|
| 63 |
+
// Session errors
|
| 64 |
+
[LEGACY.SESSION_BUSY]: "A message is being sent. Please wait.",
|
| 65 |
+
[LEGACY.SESSION_CLOSED]: "Session was closed.",
|
| 66 |
+
[LEGACY.TURN_INTERRUPTED]: "Stopped by user.",
|
| 67 |
+
|
| 68 |
+
// Protocol errors
|
| 69 |
+
[LEGACY.INVALID_JSON]: "Communication format error.",
|
| 70 |
+
[LEGACY.INVALID_REQUEST]: "Invalid request.",
|
| 71 |
+
[LEGACY.INVALID_PARAMS]: "Invalid parameters.",
|
| 72 |
+
[LEGACY.INTERNAL_ERROR]: "Internal error occurred.",
|
| 73 |
+
|
| 74 |
+
"config.invalid": "Kimi Code configuration is invalid.",
|
| 75 |
+
"model.not_configured": "No model is configured. Please sign in or configure a provider.",
|
| 76 |
+
"auth.login_required": "Authentication failed. Please sign in.",
|
| 77 |
+
"session.not_found": "Session was not found.",
|
| 78 |
+
"session.state_not_found": "Session data is missing.",
|
| 79 |
+
"session.state_invalid": "Session data is invalid.",
|
| 80 |
+
"session.init_failed": "Failed to initialize the session.",
|
| 81 |
+
"session.closed": "Session was closed.",
|
| 82 |
+
"session.fork_active_turn": "Wait for the current response before forking.",
|
| 83 |
+
"turn.agent_busy": "A message is being sent. Please wait.",
|
| 84 |
+
"provider.api_error": "Service temporarily unavailable.",
|
| 85 |
+
"provider.rate_limit": "Too many requests. Please try again later.",
|
| 86 |
+
"provider.auth_error": "Authentication failed. Please sign in again.",
|
| 87 |
+
"provider.connection_error": "Could not connect to the model provider.",
|
| 88 |
+
"request.prompt_input_empty": "Prompt cannot be empty.",
|
| 89 |
+
internal: "Internal error occurred.",
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
export function classifyError(code: string): ErrorPhase {
|
| 93 |
+
return PREFLIGHT_CODES.has(code) ? "preflight" : "runtime";
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
export function getUserMessage(code: string, fallback?: string): string {
|
| 97 |
+
return ERROR_MESSAGES[code] || fallback || "An unknown error occurred.";
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export function isPreflightError(code: string): boolean {
|
| 101 |
+
return PREFLIGHT_CODES.has(code);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
export function isUserInterrupt(code: string): boolean {
|
| 105 |
+
return code === LEGACY.TURN_INTERRUPTED || code === "turn.cancelled";
|
| 106 |
+
}
|
apps/vscode/shared/fork-turn-index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface ForkTurnItem {
|
| 2 |
+
readonly type: string;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
interface ForkTurnMessage {
|
| 6 |
+
readonly role: "user" | "assistant";
|
| 7 |
+
readonly forkable?: boolean;
|
| 8 |
+
readonly steps?: readonly {
|
| 9 |
+
readonly items: readonly ForkTurnItem[];
|
| 10 |
+
}[];
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
/** Return the core's zero-based user-visible turn index for an assistant bubble. */
|
| 14 |
+
export function getForkTurnIndex(
|
| 15 |
+
messages: readonly ForkTurnMessage[],
|
| 16 |
+
messageIndex: number,
|
| 17 |
+
): number | undefined {
|
| 18 |
+
const target = messages[messageIndex];
|
| 19 |
+
if (target?.role !== "assistant" || target.forkable === false) return undefined;
|
| 20 |
+
|
| 21 |
+
let visibleTurns = 0;
|
| 22 |
+
for (let index = 0; index <= messageIndex; index += 1) {
|
| 23 |
+
const message = messages[index];
|
| 24 |
+
if (message === undefined) continue;
|
| 25 |
+
if (message.role === "user" && message.forkable !== false) {
|
| 26 |
+
visibleTurns += 1;
|
| 27 |
+
continue;
|
| 28 |
+
}
|
| 29 |
+
if (message.role === "assistant") {
|
| 30 |
+
visibleTurns += countSteers(message);
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
return visibleTurns - 1;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
function countSteers(message: ForkTurnMessage): number {
|
| 37 |
+
return (
|
| 38 |
+
message.steps?.reduce(
|
| 39 |
+
(count, step) => count + step.items.filter((item) => item.type === "steer").length,
|
| 40 |
+
0,
|
| 41 |
+
) ?? 0
|
| 42 |
+
);
|
| 43 |
+
}
|
apps/vscode/shared/legacy-sdk.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* UI-facing compatibility types for the released Webview.
|
| 3 |
+
*
|
| 4 |
+
* The extension host adapts v1 Node SDK events into this shape while the UI is
|
| 5 |
+
* migrated without a visual rewrite. This file contains no legacy SDK runtime.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
export type ApprovalResponse = 'approve' | 'approve_for_session' | 'reject';
|
| 9 |
+
|
| 10 |
+
export type ContentPart =
|
| 11 |
+
| { type: 'text'; text: string }
|
| 12 |
+
| { type: 'think'; think: string; encrypted?: string | null }
|
| 13 |
+
| { type: 'image_url'; image_url: { url: string; id?: string | null } }
|
| 14 |
+
| { type: 'audio_url'; audio_url: { url: string; id?: string | null } }
|
| 15 |
+
| { type: 'video_url'; video_url: { url: string; id?: string | null } };
|
| 16 |
+
|
| 17 |
+
export interface BriefBlock {
|
| 18 |
+
type: 'brief';
|
| 19 |
+
text: string;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface DiffBlock {
|
| 23 |
+
type: 'diff';
|
| 24 |
+
path: string;
|
| 25 |
+
old_text: string;
|
| 26 |
+
new_text: string;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface TodoBlock {
|
| 30 |
+
type: 'todo';
|
| 31 |
+
items: Array<{ title: string; status: 'pending' | 'in_progress' | 'done' }>;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface ShellBlock {
|
| 35 |
+
type: 'shell';
|
| 36 |
+
language: string;
|
| 37 |
+
command: string;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export interface UnknownBlock {
|
| 41 |
+
type: string;
|
| 42 |
+
data?: Record<string, unknown>;
|
| 43 |
+
[key: string]: unknown;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export type DisplayBlock = BriefBlock | DiffBlock | TodoBlock | ShellBlock | UnknownBlock;
|
| 47 |
+
|
| 48 |
+
export interface ToolCall {
|
| 49 |
+
type: 'function';
|
| 50 |
+
id: string;
|
| 51 |
+
function: { name: string; arguments?: string | null };
|
| 52 |
+
extras?: Record<string, unknown> | null;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export interface ToolReturnValue {
|
| 56 |
+
is_error: boolean;
|
| 57 |
+
output: string | ContentPart[];
|
| 58 |
+
message: string;
|
| 59 |
+
display: DisplayBlock[];
|
| 60 |
+
extras?: Record<string, unknown> | null;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export interface ToolResult {
|
| 64 |
+
tool_call_id: string;
|
| 65 |
+
return_value: ToolReturnValue;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export interface TurnBegin {
|
| 69 |
+
user_input: string | ContentPart[];
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export interface TokenUsage {
|
| 73 |
+
input_other: number;
|
| 74 |
+
output: number;
|
| 75 |
+
input_cache_read: number;
|
| 76 |
+
input_cache_creation: number;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export interface StatusUpdate {
|
| 80 |
+
context_usage?: number | null;
|
| 81 |
+
token_usage?: TokenUsage | null;
|
| 82 |
+
message_id?: string | null;
|
| 83 |
+
plan_mode?: boolean | null;
|
| 84 |
+
model?: string | null;
|
| 85 |
+
thinking_effort?: string | null;
|
| 86 |
+
retrying?: {
|
| 87 |
+
next_attempt: number;
|
| 88 |
+
max_attempts: number;
|
| 89 |
+
delay_ms: number;
|
| 90 |
+
message: string;
|
| 91 |
+
} | null;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
export interface ApprovalRequestPayload {
|
| 95 |
+
id: string;
|
| 96 |
+
tool_call_id: string;
|
| 97 |
+
sender: string;
|
| 98 |
+
action: string;
|
| 99 |
+
description: string;
|
| 100 |
+
display?: DisplayBlock[];
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
export interface QuestionOption {
|
| 104 |
+
label: string;
|
| 105 |
+
description?: string;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
export interface QuestionItem {
|
| 109 |
+
question: string;
|
| 110 |
+
header?: string;
|
| 111 |
+
options: QuestionOption[];
|
| 112 |
+
multi_select?: boolean;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
export interface QuestionRequest {
|
| 116 |
+
id: string;
|
| 117 |
+
tool_call_id: string;
|
| 118 |
+
questions: QuestionItem[];
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
export interface QuestionResponse {
|
| 122 |
+
request_id: string;
|
| 123 |
+
answers: Record<string, string>;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
export interface SubagentEvent {
|
| 127 |
+
parent_tool_call_id: string;
|
| 128 |
+
event: LegacyWireEvent;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
export type LegacyWireEvent =
|
| 132 |
+
| { type: 'TurnBegin'; payload: TurnBegin & { forkable?: boolean } }
|
| 133 |
+
| { type: 'TurnEnd'; payload: Record<string, never> }
|
| 134 |
+
| { type: 'StepBegin'; payload: { n: number } }
|
| 135 |
+
| { type: 'StepInterrupted'; payload: Record<string, never> }
|
| 136 |
+
| { type: 'CompactionBegin'; payload: Record<string, never> }
|
| 137 |
+
| { type: 'CompactionEnd'; payload: Record<string, never> }
|
| 138 |
+
| { type: 'StatusUpdate'; payload: StatusUpdate }
|
| 139 |
+
| { type: 'ContentPart'; payload: ContentPart }
|
| 140 |
+
| { type: 'ToolCall'; payload: ToolCall }
|
| 141 |
+
| { type: 'ToolCallPart'; payload: { tool_call_id?: string; arguments_part?: string | null } }
|
| 142 |
+
| { type: 'ToolResult'; payload: ToolResult }
|
| 143 |
+
| { type: 'SteerInput'; payload: { user_input: string | ContentPart[] } }
|
| 144 |
+
| { type: 'SubagentEvent'; payload: SubagentEvent }
|
| 145 |
+
| { type: string; payload: unknown };
|
| 146 |
+
|
| 147 |
+
export type StreamEvent =
|
| 148 |
+
| LegacyWireEvent
|
| 149 |
+
| { type: 'ApprovalRequest'; payload: ApprovalRequestPayload }
|
| 150 |
+
| { type: 'QuestionRequest'; payload: QuestionRequest }
|
| 151 |
+
| { type: 'error'; code: string; message: string; raw?: string };
|
| 152 |
+
|
| 153 |
+
export interface RunResult {
|
| 154 |
+
status: 'finished' | 'cancelled' | 'max_steps_reached';
|
| 155 |
+
steps?: number;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
export interface SlashCommandInfo {
|
| 159 |
+
name: string;
|
| 160 |
+
description: string;
|
| 161 |
+
aliases: string[];
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
export interface ModelConfig {
|
| 165 |
+
id: string;
|
| 166 |
+
name: string;
|
| 167 |
+
provider: string;
|
| 168 |
+
capabilities: string[];
|
| 169 |
+
adaptive_thinking?: boolean;
|
| 170 |
+
support_efforts?: string[];
|
| 171 |
+
default_effort?: string;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
export interface KimiConfig {
|
| 175 |
+
defaultModel: string | null;
|
| 176 |
+
defaultThinking: boolean;
|
| 177 |
+
defaultThinkingEffort?: string;
|
| 178 |
+
models: ModelConfig[];
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/** Placeholder returned to the Webview instead of a stored MCP credential. */
|
| 182 |
+
export const MCP_SECRET_MASK = '••••••••';
|
| 183 |
+
|
| 184 |
+
export interface MCPServerConfig {
|
| 185 |
+
name: string;
|
| 186 |
+
transport: 'http' | 'stdio';
|
| 187 |
+
url?: string;
|
| 188 |
+
command?: string;
|
| 189 |
+
args?: string[];
|
| 190 |
+
env?: Record<string, string>;
|
| 191 |
+
headers?: Record<string, string>;
|
| 192 |
+
auth?: 'oauth';
|
| 193 |
+
bearerTokenEnvVar?: string;
|
| 194 |
+
/** Unified management view tags; absent on cores predating the management plane. */
|
| 195 |
+
source?: 'global' | 'plugin' | 'caller';
|
| 196 |
+
/** global: defining file path; plugin: plugin id. */
|
| 197 |
+
origin?: string;
|
| 198 |
+
/** false for plugin / project-layer entries — the panel hides mutating actions for them. */
|
| 199 |
+
mutable?: boolean;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
export interface UpdateMCPServerRequest {
|
| 203 |
+
originalName: string;
|
| 204 |
+
server: MCPServerConfig;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
export interface SessionInfo {
|
| 208 |
+
id: string;
|
| 209 |
+
workDir: string;
|
| 210 |
+
updatedAt: number;
|
| 211 |
+
brief: string;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
export type ThinkingMode = 'none' | 'switch' | 'always' | 'effort';
|
| 215 |
+
|
| 216 |
+
export interface MCPTestResult {
|
| 217 |
+
success: boolean;
|
| 218 |
+
output: string;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
export interface LoginResult {
|
| 222 |
+
success: boolean;
|
| 223 |
+
error?: string;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
export function formatContentOutput(output: string | ContentPart[]): string {
|
| 227 |
+
if (typeof output === 'string') return output;
|
| 228 |
+
if (!Array.isArray(output)) return JSON.stringify(output);
|
| 229 |
+
return output
|
| 230 |
+
.map((item) => item.type === 'text' ? item.text : `[${item.type}]`)
|
| 231 |
+
.filter(Boolean)
|
| 232 |
+
.join('\n');
|
| 233 |
+
}
|
apps/vscode/shared/types.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { RunResult, StreamEvent } from "./legacy-sdk";
|
| 2 |
+
|
| 3 |
+
export interface SessionConfig {
|
| 4 |
+
model: string;
|
| 5 |
+
thinking?: boolean;
|
| 6 |
+
effort?: string;
|
| 7 |
+
/**
|
| 8 |
+
* Whether the user explicitly changed the effort. Re-confirming the effort
|
| 9 |
+
* already shown is not an explicit choice: the model is persisted but the
|
| 10 |
+
* stored effort preference is left alone (mirrors the TUI's
|
| 11 |
+
* persistModelSelection). Treated as true when omitted.
|
| 12 |
+
*/
|
| 13 |
+
effortChanged?: boolean;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface ProjectFile {
|
| 17 |
+
path: string;
|
| 18 |
+
name: string;
|
| 19 |
+
isDirectory: boolean;
|
| 20 |
+
/** Matched-character offsets into `path`, for mention-style highlighting. */
|
| 21 |
+
matchPositions?: number[];
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export interface FileChange {
|
| 25 |
+
path: string;
|
| 26 |
+
status: "Modified" | "Added" | "Deleted";
|
| 27 |
+
additions: number;
|
| 28 |
+
deletions: number;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export interface ExtensionConfig {
|
| 32 |
+
yoloMode: boolean;
|
| 33 |
+
autosave: boolean;
|
| 34 |
+
useCtrlEnterToSend: boolean;
|
| 35 |
+
enableNewConversationShortcut: boolean;
|
| 36 |
+
showThinkingContent: boolean;
|
| 37 |
+
showThinkingExpanded: boolean;
|
| 38 |
+
version: string;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export interface WorkspaceStatus {
|
| 42 |
+
hasWorkspace: boolean;
|
| 43 |
+
path?: string;
|
| 44 |
+
workspaceRoot?: string;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export type ErrorPhase = "preflight" | "runtime";
|
| 48 |
+
|
| 49 |
+
export interface StreamError {
|
| 50 |
+
type: "error";
|
| 51 |
+
code: string;
|
| 52 |
+
message: string;
|
| 53 |
+
detail?: string; // 原始服务器错误信息
|
| 54 |
+
phase: ErrorPhase;
|
| 55 |
+
/**
|
| 56 |
+
* `false` marks a mid-turn warning: the turn is still running, so UIs must
|
| 57 |
+
* not treat it as turn-ending. Do not unlock the composer, offer Retry, or
|
| 58 |
+
* flush the queued messages for non-terminal errors.
|
| 59 |
+
*/
|
| 60 |
+
terminal?: boolean;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export type UIStreamEvent =
|
| 64 |
+
| { type: "session_start"; sessionId: string; model?: string; _sessionId?: string }
|
| 65 |
+
| { type: "stream_complete"; result: RunResult; _sessionId?: string }
|
| 66 |
+
| (StreamError & { _sessionId?: string })
|
| 67 |
+
| (StreamEvent & { _sessionId?: string });
|
| 68 |
+
|
| 69 |
+
export interface LoginStatus {
|
| 70 |
+
loggedIn: boolean;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export type { QuestionRequest, QuestionItem, QuestionOption, QuestionResponse } from "./legacy-sdk";
|
apps/vscode/shared/utils.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function cleanSystemTags(text: string): string {
|
| 2 |
+
return text.replace(/<system>.*?<\/system>\s*/gs, "").trim();
|
| 3 |
+
}
|
apps/vscode/test/app-init.test.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Scenario: App-level view routing after init, across login state transitions.
|
| 3 |
+
* Responsibilities: the sign-in screen must stay reachable from every state — in
|
| 4 |
+
* particular the no-models state (a managed OAuth token exists but config.toml
|
| 5 |
+
* has no models, e.g. a first login whose model provisioning failed after the
|
| 6 |
+
* device flow already persisted the token), where Reload alone can never change
|
| 7 |
+
* the on-disk state and the user would otherwise be stranded.
|
| 8 |
+
* Wiring: resolveAppView is pure; the bridge and toast boundaries are mocked away.
|
| 9 |
+
* Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts test/app-init.test.ts
|
| 10 |
+
*/
|
| 11 |
+
import { describe, expect, it, vi } from "vitest";
|
| 12 |
+
|
| 13 |
+
vi.mock("@/services", () => ({
|
| 14 |
+
bridge: {},
|
| 15 |
+
Events: {},
|
| 16 |
+
}));
|
| 17 |
+
vi.mock("@/components/ui/sonner", () => ({
|
| 18 |
+
toast: { error: vi.fn(), warning: vi.fn() },
|
| 19 |
+
}));
|
| 20 |
+
|
| 21 |
+
import { resolveAppView, type AppStatus } from "../webview-ui/src/hooks/useAppInit";
|
| 22 |
+
|
| 23 |
+
function resolve(
|
| 24 |
+
status: AppStatus,
|
| 25 |
+
options: { modelsCount?: number; skippedLogin?: boolean; showLogin?: boolean } = {},
|
| 26 |
+
) {
|
| 27 |
+
return resolveAppView({
|
| 28 |
+
status,
|
| 29 |
+
modelsCount: options.modelsCount ?? 0,
|
| 30 |
+
skippedLogin: options.skippedLogin ?? false,
|
| 31 |
+
showLogin: options.showLogin ?? false,
|
| 32 |
+
});
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
describe("resolveAppView", () => {
|
| 36 |
+
it("routes a brand-new user (no token, no models) to the login screen", () => {
|
| 37 |
+
expect(resolve("not-logged-in")).toEqual({ view: "login" });
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
it("routes a skipped login without models to no-models with a sign-in path", () => {
|
| 41 |
+
expect(resolve("not-logged-in", { skippedLogin: true })).toEqual({
|
| 42 |
+
view: "status",
|
| 43 |
+
status: "no-models",
|
| 44 |
+
canGoToLogin: true,
|
| 45 |
+
});
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
it("routes a skipped login with models to the main view", () => {
|
| 49 |
+
expect(resolve("not-logged-in", { skippedLogin: true, modelsCount: 2 })).toEqual({
|
| 50 |
+
view: "main",
|
| 51 |
+
});
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
it("keeps a sign-in path in the no-models trap (token without model config)", () => {
|
| 55 |
+
// Regression: this state previously rendered "Model setup required" with only
|
| 56 |
+
// a Reload button, making the login screen unreachable for affected users.
|
| 57 |
+
expect(resolve("no-models")).toEqual({
|
| 58 |
+
view: "status",
|
| 59 |
+
status: "no-models",
|
| 60 |
+
canGoToLogin: true,
|
| 61 |
+
});
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
it("routes to the login screen when the user asks for it from any state", () => {
|
| 65 |
+
expect(resolve("no-models", { showLogin: true })).toEqual({ view: "login" });
|
| 66 |
+
expect(resolve("ready", { showLogin: true, modelsCount: 1 })).toEqual({ view: "login" });
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
it("routes a no-models user who skips again back to no-models with a sign-in path", () => {
|
| 70 |
+
expect(resolve("no-models", { skippedLogin: true })).toEqual({
|
| 71 |
+
view: "status",
|
| 72 |
+
status: "no-models",
|
| 73 |
+
canGoToLogin: true,
|
| 74 |
+
});
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
it("routes ready to the main view", () => {
|
| 78 |
+
expect(resolve("ready", { modelsCount: 1 })).toEqual({ view: "main" });
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
it("routes non-login error statuses to status screens without a sign-in path", () => {
|
| 82 |
+
for (const status of ["loading", "no-workspace", "runtime-error"] as const) {
|
| 83 |
+
expect(resolve(status)).toEqual({ view: "status", status, canGoToLogin: false });
|
| 84 |
+
}
|
| 85 |
+
});
|
| 86 |
+
});
|
apps/vscode/test/baseline.manager.test.ts
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Scenario: VSCode-owned file baselines for new, migrated, and forked sessions.
|
| 3 |
+
* Responsibilities: capture originals, show changes, keep/undo, persist legacy tombstones, and reject unsafe paths.
|
| 4 |
+
* Wiring: real temporary workspace/global-storage/legacy files; no stubbed collaborators.
|
| 5 |
+
* Run: pnpm --filter kimi-code test -- baseline.manager.test.ts
|
| 6 |
+
*/
|
| 7 |
+
import { existsSync, writeFileSync } from 'node:fs';
|
| 8 |
+
import { chmod, mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises';
|
| 9 |
+
import { tmpdir } from 'node:os';
|
| 10 |
+
import { join } from 'node:path';
|
| 11 |
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
| 12 |
+
|
| 13 |
+
import {
|
| 14 |
+
BaselineManager,
|
| 15 |
+
type BaselineSession,
|
| 16 |
+
} from '../src/managers/baseline.manager';
|
| 17 |
+
|
| 18 |
+
let root: string;
|
| 19 |
+
let workDir: string;
|
| 20 |
+
let storageRoot: string;
|
| 21 |
+
let manager: BaselineManager;
|
| 22 |
+
|
| 23 |
+
beforeEach(async () => {
|
| 24 |
+
root = await mkdtemp(join(tmpdir(), 'vscode-baseline-'));
|
| 25 |
+
workDir = join(root, 'workspace');
|
| 26 |
+
storageRoot = join(root, 'global-storage');
|
| 27 |
+
await mkdir(workDir, { recursive: true });
|
| 28 |
+
manager = new BaselineManager(storageRoot);
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
afterEach(async () => {
|
| 32 |
+
await rm(root, { recursive: true, force: true });
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
describe('file baselines (capture, compare, keep, and undo)', () => {
|
| 36 |
+
it('keeps the first original when overlapping captures race', async () => {
|
| 37 |
+
const session = createSession();
|
| 38 |
+
const filePath = join(workDir, 'src', 'app.ts');
|
| 39 |
+
await mkdir(join(workDir, 'src'), { recursive: true });
|
| 40 |
+
writeFileSync(filePath, 'original\n', 'utf-8');
|
| 41 |
+
|
| 42 |
+
const firstCapture = manager.capture(session, filePath);
|
| 43 |
+
writeFileSync(filePath, 'after first edit\n', 'utf-8');
|
| 44 |
+
const secondCapture = manager.capture(session, filePath);
|
| 45 |
+
await Promise.all([firstCapture, secondCapture]);
|
| 46 |
+
|
| 47 |
+
expect(await manager.getContent(session, filePath)).toBe('original\n');
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
it('returns the captured original after the manager is recreated', async () => {
|
| 51 |
+
const session = createSession();
|
| 52 |
+
const filePath = join(workDir, 'app.ts');
|
| 53 |
+
await writeFile(filePath, 'persisted original\n', 'utf-8');
|
| 54 |
+
await manager.capture(session, filePath);
|
| 55 |
+
|
| 56 |
+
const reloadedManager = new BaselineManager(storageRoot);
|
| 57 |
+
|
| 58 |
+
await expect(reloadedManager.getContent(session, filePath)).resolves.toBe(
|
| 59 |
+
'persisted original\n',
|
| 60 |
+
);
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
it('isolates the same session id between different Kimi homes', async () => {
|
| 64 |
+
const session = createSession();
|
| 65 |
+
const filePath = join(workDir, 'app.ts');
|
| 66 |
+
const firstHome = new BaselineManager(storageRoot, join(root, 'home-a'));
|
| 67 |
+
const secondHome = new BaselineManager(storageRoot, join(root, 'home-b'));
|
| 68 |
+
await writeFile(filePath, 'home A original\n', 'utf-8');
|
| 69 |
+
await firstHome.capture(session, filePath);
|
| 70 |
+
await writeFile(filePath, 'home B original\n', 'utf-8');
|
| 71 |
+
await secondHome.capture(session, filePath);
|
| 72 |
+
|
| 73 |
+
await expect(firstHome.getContent(session, filePath)).resolves.toBe('home A original\n');
|
| 74 |
+
await expect(secondHome.getContent(session, filePath)).resolves.toBe('home B original\n');
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
it('reports a modified file when current content differs from its original', async () => {
|
| 78 |
+
const session = createSession();
|
| 79 |
+
const filePath = join(workDir, 'app.ts');
|
| 80 |
+
await writeFile(filePath, 'before\n', 'utf-8');
|
| 81 |
+
await manager.capture(session, filePath);
|
| 82 |
+
await writeFile(filePath, 'after\n', 'utf-8');
|
| 83 |
+
|
| 84 |
+
await expect(manager.getChanges(session)).resolves.toEqual([
|
| 85 |
+
{ path: 'app.ts', status: 'Modified', additions: 1, deletions: 1 },
|
| 86 |
+
]);
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
it('reports an added file when it did not exist at capture time', async () => {
|
| 90 |
+
const session = createSession();
|
| 91 |
+
const filePath = join(workDir, 'new.ts');
|
| 92 |
+
await manager.capture(session, filePath);
|
| 93 |
+
await writeFile(filePath, 'new content\n', 'utf-8');
|
| 94 |
+
|
| 95 |
+
await expect(manager.getChanges(session)).resolves.toEqual([
|
| 96 |
+
{ path: 'new.ts', status: 'Added', additions: 2, deletions: 0 },
|
| 97 |
+
]);
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
it('reports a deleted file when an existing original is removed', async () => {
|
| 101 |
+
const session = createSession();
|
| 102 |
+
const filePath = join(workDir, 'old.ts');
|
| 103 |
+
await writeFile(filePath, 'line one\nline two', 'utf-8');
|
| 104 |
+
await manager.capture(session, filePath);
|
| 105 |
+
await unlink(filePath);
|
| 106 |
+
|
| 107 |
+
await expect(manager.getChanges(session)).resolves.toEqual([
|
| 108 |
+
{ path: 'old.ts', status: 'Deleted', additions: 0, deletions: 2 },
|
| 109 |
+
]);
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
it('removes a newly created file when undo restores its missing original', async () => {
|
| 113 |
+
const session = createSession();
|
| 114 |
+
const filePath = join(workDir, 'new.ts');
|
| 115 |
+
await manager.capture(session, filePath);
|
| 116 |
+
await writeFile(filePath, 'created\n', 'utf-8');
|
| 117 |
+
|
| 118 |
+
await manager.undo(session, filePath);
|
| 119 |
+
|
| 120 |
+
expect(existsSync(filePath)).toBe(false);
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
it('restores an existing empty file without deleting it when undo runs', async () => {
|
| 124 |
+
const session = createSession();
|
| 125 |
+
const filePath = join(workDir, 'empty.ts');
|
| 126 |
+
await writeFile(filePath, '', 'utf-8');
|
| 127 |
+
await manager.capture(session, filePath);
|
| 128 |
+
await writeFile(filePath, 'changed', 'utf-8');
|
| 129 |
+
|
| 130 |
+
await manager.undo(session, filePath);
|
| 131 |
+
|
| 132 |
+
expect(existsSync(filePath)).toBe(true);
|
| 133 |
+
await expect(readFile(filePath, 'utf-8')).resolves.toBe('');
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
it('preserves CRLF bytes when undo restores the original file', async () => {
|
| 137 |
+
const session = createSession();
|
| 138 |
+
const filePath = join(workDir, 'windows.txt');
|
| 139 |
+
await writeFile(filePath, 'first\r\nsecond\r\n', 'utf-8');
|
| 140 |
+
await manager.capture(session, filePath);
|
| 141 |
+
await writeFile(filePath, 'changed\n', 'utf-8');
|
| 142 |
+
|
| 143 |
+
await manager.undo(session, filePath);
|
| 144 |
+
|
| 145 |
+
await expect(readFile(filePath, 'utf-8')).resolves.toBe('first\r\nsecond\r\n');
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
it('captures a new original after keep accepts the previous changes', async () => {
|
| 149 |
+
const session = createSession();
|
| 150 |
+
const filePath = join(workDir, 'app.ts');
|
| 151 |
+
await writeFile(filePath, 'first\n', 'utf-8');
|
| 152 |
+
await manager.capture(session, filePath);
|
| 153 |
+
await writeFile(filePath, 'accepted\n', 'utf-8');
|
| 154 |
+
await manager.keep(session, filePath);
|
| 155 |
+
|
| 156 |
+
await manager.capture(session, filePath);
|
| 157 |
+
await writeFile(filePath, 'next edit\n', 'utf-8');
|
| 158 |
+
|
| 159 |
+
expect(await manager.getContent(session, filePath)).toBe('accepted\n');
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
it('restores every tracked original when undo all runs', async () => {
|
| 163 |
+
const session = createSession();
|
| 164 |
+
await writeFile(join(workDir, 'one.ts'), 'one before', 'utf-8');
|
| 165 |
+
await writeFile(join(workDir, 'two.ts'), 'two before', 'utf-8');
|
| 166 |
+
await manager.capture(session, 'one.ts');
|
| 167 |
+
await manager.capture(session, 'two.ts');
|
| 168 |
+
await writeFile(join(workDir, 'one.ts'), 'one after', 'utf-8');
|
| 169 |
+
await writeFile(join(workDir, 'two.ts'), 'two after', 'utf-8');
|
| 170 |
+
|
| 171 |
+
await manager.undoAll(session);
|
| 172 |
+
|
| 173 |
+
await expect(readFile(join(workDir, 'one.ts'), 'utf-8')).resolves.toBe('one before');
|
| 174 |
+
await expect(readFile(join(workDir, 'two.ts'), 'utf-8')).resolves.toBe('two before');
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
it('refuses undo all when a tracked directory now links outside the workspace', async () => {
|
| 178 |
+
const session = createSession();
|
| 179 |
+
const trackedDir = join(workDir, 'src');
|
| 180 |
+
const outsideDir = join(root, 'outside');
|
| 181 |
+
await mkdir(trackedDir);
|
| 182 |
+
await mkdir(outsideDir);
|
| 183 |
+
await writeFile(join(trackedDir, 'app.ts'), 'original');
|
| 184 |
+
await manager.capture(session, 'src/app.ts');
|
| 185 |
+
await rm(trackedDir, { recursive: true });
|
| 186 |
+
await writeFile(join(outsideDir, 'app.ts'), 'outside');
|
| 187 |
+
await symlink(outsideDir, trackedDir, process.platform === 'win32' ? 'junction' : 'dir');
|
| 188 |
+
|
| 189 |
+
await expect(manager.undoAll(session)).rejects.toThrow('outside the session workspace');
|
| 190 |
+
await expect(readFile(join(outsideDir, 'app.ts'), 'utf-8')).resolves.toBe('outside');
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
it('removes every visible change when keep all runs', async () => {
|
| 194 |
+
const session = createSession();
|
| 195 |
+
await writeFile(join(workDir, 'one.ts'), 'one before', 'utf-8');
|
| 196 |
+
await writeFile(join(workDir, 'two.ts'), 'two before', 'utf-8');
|
| 197 |
+
await manager.capture(session, 'one.ts');
|
| 198 |
+
await manager.capture(session, 'two.ts');
|
| 199 |
+
await writeFile(join(workDir, 'one.ts'), 'one after', 'utf-8');
|
| 200 |
+
await writeFile(join(workDir, 'two.ts'), 'two after', 'utf-8');
|
| 201 |
+
|
| 202 |
+
await manager.keepAll(session);
|
| 203 |
+
|
| 204 |
+
await expect(manager.getChanges(session)).resolves.toEqual([]);
|
| 205 |
+
});
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
+
describe('legacy baselines (fallback, tombstones, and fork isolation)', () => {
|
| 209 |
+
it('returns the legacy original when no local baseline exists', async () => {
|
| 210 |
+
const { session } = await createLegacySession('legacy original\n');
|
| 211 |
+
await writeFile(join(workDir, 'app.ts'), 'current content\n', 'utf-8');
|
| 212 |
+
|
| 213 |
+
await expect(manager.getContent(session, 'app.ts')).resolves.toBe('legacy original\n');
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
it('reports a legacy file when current content differs from its original', async () => {
|
| 217 |
+
const { session } = await createLegacySession('legacy original\n');
|
| 218 |
+
await writeFile(join(workDir, 'app.ts'), 'current content\n', 'utf-8');
|
| 219 |
+
|
| 220 |
+
await expect(manager.getChanges(session)).resolves.toEqual([
|
| 221 |
+
{ path: 'app.ts', status: 'Modified', additions: 1, deletions: 1 },
|
| 222 |
+
]);
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
it('restores a legacy original without mutating its snapshot when undo runs', async () => {
|
| 226 |
+
const { session, baselinePath } = await createLegacySession('legacy original\n');
|
| 227 |
+
await writeFile(join(workDir, 'app.ts'), 'current content\n', 'utf-8');
|
| 228 |
+
|
| 229 |
+
await manager.undo(session, 'app.ts');
|
| 230 |
+
|
| 231 |
+
await expect(readFile(join(workDir, 'app.ts'), 'utf-8')).resolves.toBe('legacy original\n');
|
| 232 |
+
await expect(readFile(baselinePath, 'utf-8')).resolves.toBe('legacy original\n');
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
it('persists a keep tombstone for legacy content without deleting the old snapshot', async () => {
|
| 236 |
+
const { session, baselinePath } = await createLegacySession('legacy original\n');
|
| 237 |
+
await writeFile(join(workDir, 'app.ts'), 'accepted content\n', 'utf-8');
|
| 238 |
+
await manager.keep(session, 'app.ts');
|
| 239 |
+
|
| 240 |
+
const reloadedManager = new BaselineManager(storageRoot);
|
| 241 |
+
|
| 242 |
+
await expect(reloadedManager.getChanges(session)).resolves.toEqual([]);
|
| 243 |
+
await expect(readFile(baselinePath, 'utf-8')).resolves.toBe('legacy original\n');
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
it('uses a materialized local original before the fork target legacy fallback', async () => {
|
| 247 |
+
const source = createSession('ses-source');
|
| 248 |
+
const filePath = join(workDir, 'app.ts');
|
| 249 |
+
await writeFile(filePath, 'local original\n', 'utf-8');
|
| 250 |
+
await manager.capture(source, filePath);
|
| 251 |
+
const { session: target } = await createLegacySession('legacy original\n', 'ses-target');
|
| 252 |
+
|
| 253 |
+
await manager.materializeToFork(source, target);
|
| 254 |
+
|
| 255 |
+
await expect(manager.getContent(target, 'app.ts')).resolves.toBe('local original\n');
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
it('keeps a fork baseline readable when the legacy source is later deleted', async () => {
|
| 259 |
+
const { session: source, legacySessionDir } = await createLegacySession(
|
| 260 |
+
'legacy original\n',
|
| 261 |
+
'ses-source',
|
| 262 |
+
);
|
| 263 |
+
const target = createSession('ses-target', source.metadata);
|
| 264 |
+
await manager.materializeToFork(source, target);
|
| 265 |
+
await rm(legacySessionDir, { recursive: true, force: true });
|
| 266 |
+
|
| 267 |
+
await expect(manager.getContent(target, 'app.ts')).resolves.toBe('legacy original\n');
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
it('keeps the source baseline visible when the fork accepts its own copy', async () => {
|
| 271 |
+
const { session: source } = await createLegacySession('legacy original\n', 'ses-source');
|
| 272 |
+
const target = createSession('ses-target', source.metadata);
|
| 273 |
+
await manager.materializeToFork(source, target);
|
| 274 |
+
|
| 275 |
+
await manager.keep(target, 'app.ts');
|
| 276 |
+
|
| 277 |
+
await expect(manager.getContent(source, 'app.ts')).resolves.toBe('legacy original\n');
|
| 278 |
+
});
|
| 279 |
+
|
| 280 |
+
it('preserves accepted legacy paths when a fork is materialized', async () => {
|
| 281 |
+
const { session: source } = await createLegacySession('legacy original\n', 'ses-source');
|
| 282 |
+
const target = createSession('ses-target', source.metadata);
|
| 283 |
+
await manager.keep(source, 'app.ts');
|
| 284 |
+
|
| 285 |
+
await manager.materializeToFork(source, target);
|
| 286 |
+
|
| 287 |
+
await expect(manager.getContent(target, 'app.ts')).rejects.toThrow('No baseline exists');
|
| 288 |
+
});
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
describe('baseline boundaries (errors, cleanup, and platform paths)', () => {
|
| 292 |
+
it.skipIf(process.platform === 'win32')(
|
| 293 |
+
'rejects an unreadable original without recording an empty baseline',
|
| 294 |
+
async () => {
|
| 295 |
+
const session = createSession();
|
| 296 |
+
const filePath = join(workDir, 'unreadable.ts');
|
| 297 |
+
await writeFile(filePath, 'original', 'utf-8');
|
| 298 |
+
await chmod(filePath, 0o000);
|
| 299 |
+
|
| 300 |
+
try {
|
| 301 |
+
await expect(manager.capture(session, filePath)).rejects.toThrow(
|
| 302 |
+
'Unable to capture original file',
|
| 303 |
+
);
|
| 304 |
+
await expect(manager.getContent(session, filePath)).rejects.toThrow(
|
| 305 |
+
'No baseline exists',
|
| 306 |
+
);
|
| 307 |
+
} finally {
|
| 308 |
+
await chmod(filePath, 0o600);
|
| 309 |
+
}
|
| 310 |
+
},
|
| 311 |
+
);
|
| 312 |
+
|
| 313 |
+
it('rejects a directory path without recording an empty baseline', async () => {
|
| 314 |
+
const session = createSession();
|
| 315 |
+
const directoryPath = join(workDir, 'not-a-file');
|
| 316 |
+
await mkdir(directoryPath);
|
| 317 |
+
|
| 318 |
+
await expect(manager.capture(session, directoryPath)).rejects.toThrow(
|
| 319 |
+
'is not a regular file',
|
| 320 |
+
);
|
| 321 |
+
await expect(manager.getContent(session, directoryPath)).rejects.toThrow(
|
| 322 |
+
'No baseline exists',
|
| 323 |
+
);
|
| 324 |
+
});
|
| 325 |
+
|
| 326 |
+
it('throws a clear error when requested baseline content does not exist', async () => {
|
| 327 |
+
await expect(manager.getContent(createSession(), 'missing.ts')).rejects.toThrow(
|
| 328 |
+
'No baseline exists for "missing.ts"',
|
| 329 |
+
);
|
| 330 |
+
});
|
| 331 |
+
|
| 332 |
+
it('rejects a relative path that escapes the workspace', async () => {
|
| 333 |
+
await expect(manager.capture(createSession(), '../outside.ts')).rejects.toThrow(
|
| 334 |
+
'is outside workspace',
|
| 335 |
+
);
|
| 336 |
+
});
|
| 337 |
+
|
| 338 |
+
it('normalizes Windows drive case for an in-workspace baseline', async () => {
|
| 339 |
+
const session: BaselineSession = { id: 'ses-windows', workDir: 'C:\\Workspace' };
|
| 340 |
+
await manager.capture(session, 'C:\\Workspace\\src\\new.ts');
|
| 341 |
+
|
| 342 |
+
await expect(
|
| 343 |
+
manager.getContent(session, 'c:\\WORKSPACE\\SRC\\NEW.ts'),
|
| 344 |
+
).resolves.toBe('');
|
| 345 |
+
});
|
| 346 |
+
|
| 347 |
+
it('rejects a Windows path on another drive', async () => {
|
| 348 |
+
const session: BaselineSession = { id: 'ses-windows', workDir: 'C:\\Workspace' };
|
| 349 |
+
|
| 350 |
+
await expect(manager.capture(session, 'D:\\Workspace\\file.ts')).rejects.toThrow(
|
| 351 |
+
'is outside workspace',
|
| 352 |
+
);
|
| 353 |
+
});
|
| 354 |
+
|
| 355 |
+
it('normalizes case differences in an in-workspace UNC path', async () => {
|
| 356 |
+
const session: BaselineSession = {
|
| 357 |
+
id: 'ses-unc',
|
| 358 |
+
workDir: '\\\\Server\\Share\\Workspace',
|
| 359 |
+
};
|
| 360 |
+
await manager.capture(session, '\\\\server\\share\\workspace\\src\\new.ts');
|
| 361 |
+
|
| 362 |
+
await expect(manager.getContent(session, 'src\\new.ts')).resolves.toBe('');
|
| 363 |
+
});
|
| 364 |
+
|
| 365 |
+
it('rejects a UNC path from another share', async () => {
|
| 366 |
+
const session: BaselineSession = {
|
| 367 |
+
id: 'ses-unc',
|
| 368 |
+
workDir: '\\\\Server\\Share\\Workspace',
|
| 369 |
+
};
|
| 370 |
+
|
| 371 |
+
await expect(
|
| 372 |
+
manager.capture(session, '\\\\Server\\OtherShare\\Workspace\\file.ts'),
|
| 373 |
+
).rejects.toThrow('is outside workspace');
|
| 374 |
+
});
|
| 375 |
+
|
| 376 |
+
it('removes persisted originals when the session baseline is deleted', async () => {
|
| 377 |
+
const session = createSession();
|
| 378 |
+
const filePath = join(workDir, 'app.ts');
|
| 379 |
+
await writeFile(filePath, 'original', 'utf-8');
|
| 380 |
+
await manager.capture(session, filePath);
|
| 381 |
+
|
| 382 |
+
await manager.deleteSession(session.id);
|
| 383 |
+
|
| 384 |
+
await expect(manager.getContent(session, filePath)).rejects.toThrow('No baseline exists');
|
| 385 |
+
});
|
| 386 |
+
});
|
| 387 |
+
|
| 388 |
+
function createSession(
|
| 389 |
+
id = 'ses-local',
|
| 390 |
+
metadata?: Readonly<Record<string, unknown>>,
|
| 391 |
+
): BaselineSession {
|
| 392 |
+
return { id, workDir, metadata };
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
async function createLegacySession(
|
| 396 |
+
content: string,
|
| 397 |
+
id = 'ses-legacy',
|
| 398 |
+
): Promise<{
|
| 399 |
+
session: BaselineSession;
|
| 400 |
+
legacySessionDir: string;
|
| 401 |
+
baselinePath: string;
|
| 402 |
+
}> {
|
| 403 |
+
const legacySessionDir = join(root, `legacy-${id}`);
|
| 404 |
+
const baselinePath = join(legacySessionDir, 'baseline', 'app.ts');
|
| 405 |
+
await mkdir(join(legacySessionDir, 'baseline'), { recursive: true });
|
| 406 |
+
await writeFile(baselinePath, content, 'utf-8');
|
| 407 |
+
return {
|
| 408 |
+
session: createSession(id, { kimi_cli_source_path: legacySessionDir }),
|
| 409 |
+
legacySessionDir,
|
| 410 |
+
baselinePath,
|
| 411 |
+
};
|
| 412 |
+
}
|
apps/vscode/test/bridge-handler.test.ts
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Scenario: untrusted Webview RPC messages cross into the VS Code extension host.
|
| 3 |
+
* Responsibilities: validate requests, preserve public model metadata, omit private paths, and recover visibly from persisted state errors.
|
| 4 |
+
* Wiring: the real BridgeHandler and handlers; VS Code and the public Node SDK harness boundary are replaced.
|
| 5 |
+
* Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/bridge-handler.test.ts
|
| 6 |
+
*/
|
| 7 |
+
import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
| 8 |
+
import { tmpdir } from "node:os";
|
| 9 |
+
import { join } from "node:path";
|
| 10 |
+
|
| 11 |
+
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
| 12 |
+
import type * as vscode from "vscode";
|
| 13 |
+
|
| 14 |
+
import { Methods } from "../shared/bridge";
|
| 15 |
+
import { BridgeHandler } from "../src/bridge-handler";
|
| 16 |
+
|
| 17 |
+
const host = vi.hoisted(() => {
|
| 18 |
+
const watcher = {
|
| 19 |
+
onDidChange: vi.fn(),
|
| 20 |
+
onDidCreate: vi.fn(),
|
| 21 |
+
onDidDelete: vi.fn(),
|
| 22 |
+
dispose: vi.fn(),
|
| 23 |
+
};
|
| 24 |
+
const harness = {
|
| 25 |
+
homeDir: "/tmp/kimi-code-test-home",
|
| 26 |
+
close: vi.fn(async () => undefined),
|
| 27 |
+
getConfig: vi.fn(),
|
| 28 |
+
setConfig: vi.fn(async () => undefined),
|
| 29 |
+
listSessions: vi.fn(async () => []),
|
| 30 |
+
resumeSession: vi.fn(),
|
| 31 |
+
forkSession: vi.fn(),
|
| 32 |
+
deleteSession: vi.fn(async () => undefined),
|
| 33 |
+
};
|
| 34 |
+
const showWarningMessage = vi.fn(async () => undefined as string | undefined);
|
| 35 |
+
|
| 36 |
+
class Uri {
|
| 37 |
+
readonly scheme = "file";
|
| 38 |
+
readonly authority = "";
|
| 39 |
+
readonly path: string;
|
| 40 |
+
|
| 41 |
+
constructor(readonly fsPath: string) {
|
| 42 |
+
this.path = fsPath;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
static joinPath(base: Uri, ...segments: string[]): Uri {
|
| 46 |
+
return new Uri(join(base.fsPath, ...segments));
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
toString(): string {
|
| 50 |
+
return `file://${this.path}`;
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return {
|
| 55 |
+
Uri,
|
| 56 |
+
watcher,
|
| 57 |
+
harness,
|
| 58 |
+
createKimiHarness: vi.fn(() => harness),
|
| 59 |
+
showWarningMessage,
|
| 60 |
+
workspaceFolders: [] as Array<{ uri: Uri }>,
|
| 61 |
+
};
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
vi.mock("vscode", () => ({
|
| 65 |
+
Uri: host.Uri,
|
| 66 |
+
workspace: {
|
| 67 |
+
get workspaceFolders() {
|
| 68 |
+
return host.workspaceFolders;
|
| 69 |
+
},
|
| 70 |
+
getConfiguration: () => ({ get: (_key: string, fallback: unknown) => fallback }),
|
| 71 |
+
createFileSystemWatcher: () => host.watcher,
|
| 72 |
+
textDocuments: [],
|
| 73 |
+
},
|
| 74 |
+
window: { showWarningMessage: host.showWarningMessage },
|
| 75 |
+
}));
|
| 76 |
+
|
| 77 |
+
vi.mock("@moonshot-ai/kimi-code-sdk", async (importOriginal) => {
|
| 78 |
+
const original = await importOriginal<typeof import("@moonshot-ai/kimi-code-sdk")>();
|
| 79 |
+
return {
|
| 80 |
+
...original,
|
| 81 |
+
createKimiHarness: () => host.createKimiHarness(),
|
| 82 |
+
};
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
let bridge: BridgeHandler;
|
| 86 |
+
let root: string;
|
| 87 |
+
let showLogs: Mock<() => void>;
|
| 88 |
+
let writeLog: Mock<(message: string) => void>;
|
| 89 |
+
let workspaceState: { get: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
|
| 90 |
+
|
| 91 |
+
beforeEach(async () => {
|
| 92 |
+
root = await mkdtemp(join(tmpdir(), "kimi-vscode-bridge-"));
|
| 93 |
+
host.workspaceFolders.splice(0, host.workspaceFolders.length, { uri: new host.Uri(root) });
|
| 94 |
+
showLogs = vi.fn();
|
| 95 |
+
writeLog = vi.fn();
|
| 96 |
+
host.harness.resumeSession.mockReset();
|
| 97 |
+
host.harness.getConfig.mockReset();
|
| 98 |
+
host.harness.getConfig.mockResolvedValue({ models: {} });
|
| 99 |
+
host.createKimiHarness.mockImplementation(() => host.harness);
|
| 100 |
+
host.showWarningMessage.mockReset();
|
| 101 |
+
host.showWarningMessage.mockResolvedValue(undefined);
|
| 102 |
+
workspaceState = { get: vi.fn((_key, fallback) => fallback), update: vi.fn() };
|
| 103 |
+
bridge = new BridgeHandler(
|
| 104 |
+
vi.fn(),
|
| 105 |
+
workspaceState as unknown as vscode.Memento,
|
| 106 |
+
join(root, "global-storage"),
|
| 107 |
+
vi.fn(),
|
| 108 |
+
showLogs,
|
| 109 |
+
writeLog,
|
| 110 |
+
);
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
afterEach(async () => {
|
| 114 |
+
await bridge.dispose();
|
| 115 |
+
vi.clearAllMocks();
|
| 116 |
+
vi.unstubAllEnvs();
|
| 117 |
+
await rm(root, { recursive: true, force: true });
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
describe("Engine startup", () => {
|
| 121 |
+
function constructBridge(): void {
|
| 122 |
+
new BridgeHandler(
|
| 123 |
+
vi.fn(),
|
| 124 |
+
workspaceState as unknown as vscode.Memento,
|
| 125 |
+
join(root, "global-storage-2"),
|
| 126 |
+
vi.fn(),
|
| 127 |
+
showLogs,
|
| 128 |
+
writeLog,
|
| 129 |
+
);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
it("surfaces the failure when the engine cannot start", () => {
|
| 133 |
+
host.createKimiHarness.mockImplementationOnce(() => {
|
| 134 |
+
throw new Error("engine boom");
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
expect(constructBridge).toThrow(/^Failed to start the Kimi engine: engine boom\.$/);
|
| 138 |
+
});
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
describe("Webview RPC boundary (validates requests before host dispatch)", () => {
|
| 142 |
+
it("returns a readable error when the envelope is not a plain object", async () => {
|
| 143 |
+
const result = await bridge.handle([], "view-1");
|
| 144 |
+
|
| 145 |
+
expect(result).toEqual({
|
| 146 |
+
id: "",
|
| 147 |
+
error: "Invalid bridge request: expected a plain object.",
|
| 148 |
+
});
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
it("does not execute a known handler when the request id is blank", async () => {
|
| 152 |
+
const result = await bridge.handle({ id: " ", method: Methods.ShowLogs }, "view-1");
|
| 153 |
+
|
| 154 |
+
expect(result).toEqual({
|
| 155 |
+
id: "",
|
| 156 |
+
error: "Invalid bridge request: id must be a non-empty string.",
|
| 157 |
+
});
|
| 158 |
+
expect(showLogs).not.toHaveBeenCalled();
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
it("reports aborted: false when the view has no runtime to cancel", async () => {
|
| 162 |
+
const result = await bridge.handle({ id: "rpc-1", method: Methods.AbortChat }, "view-1");
|
| 163 |
+
|
| 164 |
+
expect(result).toEqual({ id: "rpc-1", result: { aborted: false } });
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
it("cancels the view's runtime when aborting a chat", async () => {
|
| 168 |
+
const cancel = vi.fn(async () => undefined);
|
| 169 |
+
vi.spyOn(bridge.runtime, "getSessionForView").mockReturnValue({ cancel } as never);
|
| 170 |
+
|
| 171 |
+
const result = await bridge.handle({ id: "rpc-1", method: Methods.AbortChat }, "view-1");
|
| 172 |
+
|
| 173 |
+
expect(result).toEqual({ id: "rpc-1", result: { aborted: true } });
|
| 174 |
+
expect(cancel).toHaveBeenCalledOnce();
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
it.each(["missingMethod", "toString", "constructor", "__proto__"])(
|
| 178 |
+
"does not dispatch the unknown or prototype method %s",
|
| 179 |
+
async (method) => {
|
| 180 |
+
const result = await bridge.handle({ id: "rpc-1", method }, "view-1");
|
| 181 |
+
|
| 182 |
+
expect(result).toEqual({ id: "rpc-1", error: `Unknown bridge method: ${method}` });
|
| 183 |
+
expect(showLogs).not.toHaveBeenCalled();
|
| 184 |
+
},
|
| 185 |
+
);
|
| 186 |
+
|
| 187 |
+
it("does not execute a no-params handler when a payload is supplied", async () => {
|
| 188 |
+
const result = await bridge.handle(
|
| 189 |
+
{ id: "rpc-1", method: Methods.ShowLogs, params: {} },
|
| 190 |
+
"view-1",
|
| 191 |
+
);
|
| 192 |
+
|
| 193 |
+
expect(result).toEqual({
|
| 194 |
+
id: "rpc-1",
|
| 195 |
+
error: "Invalid bridge params for method: showLogs",
|
| 196 |
+
});
|
| 197 |
+
expect(showLogs).not.toHaveBeenCalled();
|
| 198 |
+
});
|
| 199 |
+
|
| 200 |
+
it("does not execute an object-payload handler when a required field has the wrong type", async () => {
|
| 201 |
+
const result = await bridge.handle(
|
| 202 |
+
{ id: "rpc-1", method: Methods.AddInputHistory, params: { text: 42 } },
|
| 203 |
+
"view-1",
|
| 204 |
+
);
|
| 205 |
+
|
| 206 |
+
expect(result).toEqual({
|
| 207 |
+
id: "rpc-1",
|
| 208 |
+
error: "Invalid bridge params for method: addInputHistory",
|
| 209 |
+
});
|
| 210 |
+
expect(workspaceState.update).not.toHaveBeenCalled();
|
| 211 |
+
});
|
| 212 |
+
|
| 213 |
+
it("dispatches a valid request through the existing bridge surface", async () => {
|
| 214 |
+
const result = await bridge.handle({ id: "rpc-1", method: Methods.ShowLogs }, "view-1");
|
| 215 |
+
|
| 216 |
+
expect(result).toEqual({ id: "rpc-1", result: { ok: true } });
|
| 217 |
+
expect(showLogs).toHaveBeenCalledOnce();
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
it("keeps provider identity when configured models share a display name", async () => {
|
| 221 |
+
host.harness.getConfig.mockResolvedValueOnce({
|
| 222 |
+
defaultModel: "openai/shared",
|
| 223 |
+
models: {
|
| 224 |
+
"openai/shared": {
|
| 225 |
+
provider: "openai",
|
| 226 |
+
model: "shared",
|
| 227 |
+
displayName: "Shared",
|
| 228 |
+
maxContextSize: 128_000,
|
| 229 |
+
},
|
| 230 |
+
"proxy/shared": {
|
| 231 |
+
provider: "company-proxy",
|
| 232 |
+
model: "shared",
|
| 233 |
+
displayName: "Shared",
|
| 234 |
+
maxContextSize: 128_000,
|
| 235 |
+
},
|
| 236 |
+
},
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
const result = await bridge.handle({ id: "rpc-models", method: Methods.GetModels }, "view-1");
|
| 240 |
+
|
| 241 |
+
expect(result).toMatchObject({
|
| 242 |
+
id: "rpc-models",
|
| 243 |
+
result: {
|
| 244 |
+
defaultModel: "openai/shared",
|
| 245 |
+
models: [
|
| 246 |
+
{ id: "openai/shared", name: "Shared", provider: "openai" },
|
| 247 |
+
{ id: "proxy/shared", name: "Shared", provider: "company-proxy" },
|
| 248 |
+
],
|
| 249 |
+
},
|
| 250 |
+
});
|
| 251 |
+
});
|
| 252 |
+
|
| 253 |
+
it("preserves adaptive thinking metadata in the Webview model list", async () => {
|
| 254 |
+
host.harness.getConfig.mockResolvedValueOnce({
|
| 255 |
+
defaultModel: "anthropic/claude",
|
| 256 |
+
models: {
|
| 257 |
+
"anthropic/claude": {
|
| 258 |
+
provider: "anthropic",
|
| 259 |
+
model: "claude-sonnet",
|
| 260 |
+
maxContextSize: 200_000,
|
| 261 |
+
adaptiveThinking: true,
|
| 262 |
+
},
|
| 263 |
+
},
|
| 264 |
+
});
|
| 265 |
+
|
| 266 |
+
const result = await bridge.handle({ id: "rpc-models", method: Methods.GetModels }, "view-1");
|
| 267 |
+
|
| 268 |
+
expect(result).toMatchObject({
|
| 269 |
+
result: {
|
| 270 |
+
models: [{
|
| 271 |
+
id: "anthropic/claude",
|
| 272 |
+
name: "claude-sonnet",
|
| 273 |
+
provider: "anthropic",
|
| 274 |
+
adaptive_thinking: true,
|
| 275 |
+
}],
|
| 276 |
+
},
|
| 277 |
+
});
|
| 278 |
+
});
|
| 279 |
+
|
| 280 |
+
it("resolves the fallback-profile default effort with the provider type", async () => {
|
| 281 |
+
// claude-latest declares efforts but no default; the Anthropic fallback
|
| 282 |
+
// profile only matches when the provider type joins the resolution.
|
| 283 |
+
host.harness.getConfig.mockResolvedValueOnce({
|
| 284 |
+
defaultModel: "custom/claude",
|
| 285 |
+
providers: {
|
| 286 |
+
custom: { type: "anthropic", apiKey: "test-key" },
|
| 287 |
+
},
|
| 288 |
+
models: {
|
| 289 |
+
"custom/claude": {
|
| 290 |
+
provider: "custom",
|
| 291 |
+
model: "claude-latest",
|
| 292 |
+
supportEfforts: ["low", "medium", "high", "xhigh", "max"],
|
| 293 |
+
},
|
| 294 |
+
},
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
const result = await bridge.handle({ id: "rpc-models", method: Methods.GetModels }, "view-1");
|
| 298 |
+
|
| 299 |
+
expect(result).toMatchObject({
|
| 300 |
+
result: {
|
| 301 |
+
models: [{
|
| 302 |
+
id: "custom/claude",
|
| 303 |
+
support_efforts: ["low", "medium", "high", "xhigh", "max"],
|
| 304 |
+
default_effort: "high",
|
| 305 |
+
}],
|
| 306 |
+
},
|
| 307 |
+
});
|
| 308 |
+
});
|
| 309 |
+
|
| 310 |
+
it("does not expose the session storage path when listing sessions", async () => {
|
| 311 |
+
host.harness.listSessions.mockResolvedValueOnce([
|
| 312 |
+
{
|
| 313 |
+
id: "session-1",
|
| 314 |
+
workDir: root,
|
| 315 |
+
sessionDir: "/private/kimi/sessions/session-1",
|
| 316 |
+
updatedAt: 123,
|
| 317 |
+
title: "Visible title",
|
| 318 |
+
},
|
| 319 |
+
] as never);
|
| 320 |
+
|
| 321 |
+
const result = await bridge.handle(
|
| 322 |
+
{ id: "rpc-1", method: Methods.GetKimiSessions },
|
| 323 |
+
"view-1",
|
| 324 |
+
);
|
| 325 |
+
|
| 326 |
+
expect(result).toEqual({
|
| 327 |
+
id: "rpc-1",
|
| 328 |
+
result: [{ id: "session-1", workDir: root, updatedAt: 123, brief: "Visible title" }],
|
| 329 |
+
});
|
| 330 |
+
expect(JSON.stringify(result)).not.toContain("/private/kimi/sessions");
|
| 331 |
+
});
|
| 332 |
+
|
| 333 |
+
it("does not expose the session storage path when forking a session", async () => {
|
| 334 |
+
const source = {
|
| 335 |
+
id: "session-1",
|
| 336 |
+
workDir: root,
|
| 337 |
+
sessionDir: "/private/kimi/sessions/session-1",
|
| 338 |
+
updatedAt: 123,
|
| 339 |
+
};
|
| 340 |
+
const target = {
|
| 341 |
+
id: "session-2",
|
| 342 |
+
workDir: root,
|
| 343 |
+
sessionDir: "/private/kimi/sessions/session-2",
|
| 344 |
+
updatedAt: 124,
|
| 345 |
+
};
|
| 346 |
+
host.harness.listSessions.mockResolvedValueOnce([source] as never);
|
| 347 |
+
host.harness.forkSession.mockResolvedValueOnce({ summary: target, close: vi.fn() });
|
| 348 |
+
|
| 349 |
+
const result = await bridge.handle(
|
| 350 |
+
{
|
| 351 |
+
id: "rpc-1",
|
| 352 |
+
method: Methods.ForkKimiSession,
|
| 353 |
+
params: { sessionId: "session-1", turnIndex: 0 },
|
| 354 |
+
},
|
| 355 |
+
"view-1",
|
| 356 |
+
);
|
| 357 |
+
|
| 358 |
+
expect(result).toEqual({ id: "rpc-1", result: { sessionId: "session-2" } });
|
| 359 |
+
expect(JSON.stringify(result)).not.toContain("/private/kimi/sessions");
|
| 360 |
+
});
|
| 361 |
+
|
| 362 |
+
it("runs a fork through the active session cancellation boundary", async () => {
|
| 363 |
+
const source = {
|
| 364 |
+
id: "session-1",
|
| 365 |
+
workDir: root,
|
| 366 |
+
sessionDir: "/private/kimi/sessions/session-1",
|
| 367 |
+
updatedAt: 123,
|
| 368 |
+
};
|
| 369 |
+
const target = {
|
| 370 |
+
id: "session-2",
|
| 371 |
+
workDir: root,
|
| 372 |
+
sessionDir: "/private/kimi/sessions/session-2",
|
| 373 |
+
updatedAt: 124,
|
| 374 |
+
};
|
| 375 |
+
const runExclusiveAfterCancelling = vi.fn(async <T>(action: () => Promise<T>) => action());
|
| 376 |
+
vi.spyOn(bridge.runtime, "getSession").mockReturnValue({
|
| 377 |
+
runExclusiveAfterCancelling,
|
| 378 |
+
} as never);
|
| 379 |
+
host.harness.listSessions.mockResolvedValueOnce([source] as never);
|
| 380 |
+
host.harness.forkSession.mockResolvedValueOnce({ summary: target, close: vi.fn() });
|
| 381 |
+
|
| 382 |
+
const result = await bridge.handle(
|
| 383 |
+
{
|
| 384 |
+
id: "rpc-1",
|
| 385 |
+
method: Methods.ForkKimiSession,
|
| 386 |
+
params: { sessionId: "session-1", turnIndex: 0 },
|
| 387 |
+
},
|
| 388 |
+
"view-1",
|
| 389 |
+
);
|
| 390 |
+
|
| 391 |
+
expect(result).toEqual({ id: "rpc-1", result: { sessionId: "session-2" } });
|
| 392 |
+
expect(runExclusiveAfterCancelling).toHaveBeenCalledOnce();
|
| 393 |
+
expect(host.harness.forkSession).toHaveBeenCalledOnce();
|
| 394 |
+
});
|
| 395 |
+
|
| 396 |
+
it("closes and removes a fork when its baseline cannot be materialized", async () => {
|
| 397 |
+
const source = { id: "session-1", workDir: root, updatedAt: 123 };
|
| 398 |
+
const target = { id: "session-2", workDir: root, updatedAt: 124 };
|
| 399 |
+
const close = vi.fn(async () => undefined);
|
| 400 |
+
host.harness.listSessions.mockResolvedValueOnce([source] as never);
|
| 401 |
+
host.harness.forkSession.mockResolvedValueOnce({ summary: target, close });
|
| 402 |
+
vi.spyOn(bridge.baselineManager, "materializeToFork").mockRejectedValueOnce(
|
| 403 |
+
new Error("baseline unavailable"),
|
| 404 |
+
);
|
| 405 |
+
const deleteBaseline = vi.spyOn(bridge.baselineManager, "deleteSession");
|
| 406 |
+
|
| 407 |
+
const result = await bridge.handle(
|
| 408 |
+
{
|
| 409 |
+
id: "rpc-1",
|
| 410 |
+
method: Methods.ForkKimiSession,
|
| 411 |
+
params: { sessionId: "session-1", turnIndex: 0 },
|
| 412 |
+
},
|
| 413 |
+
"view-1",
|
| 414 |
+
);
|
| 415 |
+
|
| 416 |
+
expect(result).toEqual({ id: "rpc-1", error: "baseline unavailable" });
|
| 417 |
+
expect(close).toHaveBeenCalledOnce();
|
| 418 |
+
expect(host.harness.deleteSession).toHaveBeenCalledWith("session-2");
|
| 419 |
+
expect(deleteBaseline).toHaveBeenCalledWith("session-2");
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
it("keeps conversation history available when its baseline snapshot disappears", async () => {
|
| 423 |
+
const session = createResumedSession("session-1", root);
|
| 424 |
+
host.harness.resumeSession.mockResolvedValueOnce(session as never);
|
| 425 |
+
host.showWarningMessage.mockResolvedValueOnce("Show Logs");
|
| 426 |
+
const sourcePath = join(root, "app.ts");
|
| 427 |
+
await writeFile(sourcePath, "original\n", "utf-8");
|
| 428 |
+
await bridge.baselineManager.capture(session.summary, sourcePath);
|
| 429 |
+
const baselinesRoot = join(root, "global-storage", "baselines");
|
| 430 |
+
const [homeDirectory] = await readdir(baselinesRoot);
|
| 431 |
+
const [sessionDirectory] = await readdir(join(baselinesRoot, homeDirectory!));
|
| 432 |
+
const snapshotsDirectory = join(
|
| 433 |
+
baselinesRoot,
|
| 434 |
+
homeDirectory!,
|
| 435 |
+
sessionDirectory!,
|
| 436 |
+
"snapshots",
|
| 437 |
+
);
|
| 438 |
+
const [snapshot] = await readdir(snapshotsDirectory);
|
| 439 |
+
await rm(join(snapshotsDirectory, snapshot!));
|
| 440 |
+
|
| 441 |
+
const result = await bridge.handle(
|
| 442 |
+
{
|
| 443 |
+
id: "rpc-1",
|
| 444 |
+
method: Methods.LoadKimiSessionHistory,
|
| 445 |
+
params: { kimiSessionId: "session-1" },
|
| 446 |
+
},
|
| 447 |
+
"view-1",
|
| 448 |
+
);
|
| 449 |
+
|
| 450 |
+
expect(result).toEqual({
|
| 451 |
+
id: "rpc-1",
|
| 452 |
+
result: expect.arrayContaining([
|
| 453 |
+
expect.objectContaining({ type: "StatusUpdate", _sessionId: "session-1" }),
|
| 454 |
+
]),
|
| 455 |
+
});
|
| 456 |
+
expect(writeLog).toHaveBeenCalledWith(
|
| 457 |
+
expect.stringMatching(/Unable to restore session file changes.*Unable to read baseline snapshot/),
|
| 458 |
+
);
|
| 459 |
+
await vi.waitFor(() => expect(showLogs).toHaveBeenCalledOnce());
|
| 460 |
+
});
|
| 461 |
+
|
| 462 |
+
it("returns a readable error when persisted session state is corrupt without wedging the bridge", async () => {
|
| 463 |
+
host.harness.resumeSession.mockRejectedValueOnce(
|
| 464 |
+
new Error("Session state is invalid JSON at line 4"),
|
| 465 |
+
);
|
| 466 |
+
|
| 467 |
+
const failed = await bridge.handle(
|
| 468 |
+
{
|
| 469 |
+
id: "rpc-1",
|
| 470 |
+
method: Methods.LoadKimiSessionHistory,
|
| 471 |
+
params: { kimiSessionId: "session-1" },
|
| 472 |
+
},
|
| 473 |
+
"view-1",
|
| 474 |
+
);
|
| 475 |
+
const next = await bridge.handle({ id: "rpc-2", method: Methods.ShowLogs }, "view-1");
|
| 476 |
+
|
| 477 |
+
expect(failed).toEqual({
|
| 478 |
+
id: "rpc-1",
|
| 479 |
+
error: "Session state is invalid JSON at line 4",
|
| 480 |
+
});
|
| 481 |
+
expect(writeLog).toHaveBeenCalledWith(
|
| 482 |
+
expect.stringContaining("Session state is invalid JSON at line 4"),
|
| 483 |
+
);
|
| 484 |
+
expect(next).toEqual({ id: "rpc-2", result: { ok: true } });
|
| 485 |
+
});
|
| 486 |
+
});
|
| 487 |
+
|
| 488 |
+
describe("Registered working directories", () => {
|
| 489 |
+
it("lists the workspace root when there is no session history", async () => {
|
| 490 |
+
host.harness.listSessions.mockResolvedValueOnce([] as never);
|
| 491 |
+
|
| 492 |
+
const result = await bridge.handle({ id: "rpc-1", method: Methods.GetRegisteredWorkDirs }, "view-1");
|
| 493 |
+
|
| 494 |
+
expect(result).toEqual({ id: "rpc-1", result: [root] });
|
| 495 |
+
});
|
| 496 |
+
|
| 497 |
+
it("keeps the selected working directory visible without session history", async () => {
|
| 498 |
+
const sub = join(root, "packages", "demo");
|
| 499 |
+
await mkdir(sub, { recursive: true });
|
| 500 |
+
host.harness.listSessions.mockResolvedValue([] as never);
|
| 501 |
+
|
| 502 |
+
await bridge.handle({ id: "rpc-1", method: Methods.SetWorkDir, params: { workDir: sub } }, "view-1");
|
| 503 |
+
const result = await bridge.handle({ id: "rpc-2", method: Methods.GetRegisteredWorkDirs }, "view-1");
|
| 504 |
+
|
| 505 |
+
expect(result).toEqual({ id: "rpc-2", result: [root, sub].toSorted() });
|
| 506 |
+
});
|
| 507 |
+
|
| 508 |
+
it("merges session-history directories and hides directories outside the workspace", async () => {
|
| 509 |
+
const inside = join(root, "nested");
|
| 510 |
+
host.harness.listSessions.mockResolvedValueOnce([
|
| 511 |
+
{ id: "s-1", workDir: inside },
|
| 512 |
+
{ id: "s-2", workDir: "/private/outside" },
|
| 513 |
+
{ id: "s-3", workDir: root },
|
| 514 |
+
] as never);
|
| 515 |
+
|
| 516 |
+
const result = await bridge.handle({ id: "rpc-1", method: Methods.GetRegisteredWorkDirs }, "view-1");
|
| 517 |
+
|
| 518 |
+
expect(result).toEqual({ id: "rpc-1", result: [inside, root].toSorted() });
|
| 519 |
+
expect(JSON.stringify(result)).not.toContain("/private/outside");
|
| 520 |
+
});
|
| 521 |
+
});
|
| 522 |
+
|
| 523 |
+
describe("Webview config saves (thinking effort persistence parity with the TUI)", () => {
|
| 524 |
+
const effortModel = {
|
| 525 |
+
provider: "managed:kimi-code",
|
| 526 |
+
model: "reasoning",
|
| 527 |
+
supportEfforts: ["low", "high", "max"],
|
| 528 |
+
defaultEffort: "high",
|
| 529 |
+
};
|
| 530 |
+
|
| 531 |
+
function mockConfig(thinking?: { enabled: boolean; effort?: string }) {
|
| 532 |
+
host.harness.getConfig.mockResolvedValue({
|
| 533 |
+
defaultModel: "kimi/reasoning",
|
| 534 |
+
thinking,
|
| 535 |
+
models: { "kimi/reasoning": effortModel },
|
| 536 |
+
} as never);
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
it("persists a non-top effort as the global default", async () => {
|
| 540 |
+
mockConfig();
|
| 541 |
+
|
| 542 |
+
const result = await bridge.handle(
|
| 543 |
+
{ id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "high" } },
|
| 544 |
+
"view-1",
|
| 545 |
+
);
|
| 546 |
+
|
| 547 |
+
expect(result).toEqual({ id: "rpc-1", result: { ok: true } });
|
| 548 |
+
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
| 549 |
+
defaultModel: "kimi/reasoning",
|
| 550 |
+
thinking: { enabled: true, effort: "high" },
|
| 551 |
+
});
|
| 552 |
+
});
|
| 553 |
+
|
| 554 |
+
it("keeps a pick above the model's delivered default session-only", async () => {
|
| 555 |
+
mockConfig();
|
| 556 |
+
|
| 557 |
+
await bridge.handle(
|
| 558 |
+
{ id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "max" } },
|
| 559 |
+
"view-1",
|
| 560 |
+
);
|
| 561 |
+
|
| 562 |
+
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
| 563 |
+
defaultModel: "kimi/reasoning",
|
| 564 |
+
thinking: { enabled: true },
|
| 565 |
+
});
|
| 566 |
+
});
|
| 567 |
+
|
| 568 |
+
it("persists the top tier when the model's delivered default is the top tier", async () => {
|
| 569 |
+
host.harness.getConfig.mockResolvedValue({
|
| 570 |
+
defaultModel: "kimi/reasoning",
|
| 571 |
+
models: { "kimi/reasoning": { ...effortModel, defaultEffort: "max" } },
|
| 572 |
+
} as never);
|
| 573 |
+
|
| 574 |
+
await bridge.handle(
|
| 575 |
+
{ id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "max" } },
|
| 576 |
+
"view-1",
|
| 577 |
+
);
|
| 578 |
+
|
| 579 |
+
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
| 580 |
+
defaultModel: "kimi/reasoning",
|
| 581 |
+
thinking: { enabled: true, effort: "max" },
|
| 582 |
+
});
|
| 583 |
+
});
|
| 584 |
+
|
| 585 |
+
it("keeps an xhigh pick session-only when the default comes from the Anthropic profile inference", async () => {
|
| 586 |
+
// claude-opus-4-7 declares no efforts; the profile inference supplies
|
| 587 |
+
// [low, medium, high, xhigh, max] and resolves the default to "high".
|
| 588 |
+
host.harness.getConfig.mockResolvedValue({
|
| 589 |
+
defaultModel: "custom/claude",
|
| 590 |
+
models: { "custom/claude": { provider: "custom", model: "claude-opus-4-7" } },
|
| 591 |
+
} as never);
|
| 592 |
+
|
| 593 |
+
await bridge.handle(
|
| 594 |
+
{ id: "rpc-1", method: Methods.SaveConfig, params: { model: "custom/claude", thinking: true, effort: "xhigh" } },
|
| 595 |
+
"view-1",
|
| 596 |
+
);
|
| 597 |
+
|
| 598 |
+
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
| 599 |
+
defaultModel: "custom/claude",
|
| 600 |
+
thinking: { enabled: true },
|
| 601 |
+
});
|
| 602 |
+
});
|
| 603 |
+
|
| 604 |
+
it("persists the concrete effort when the model's levels are unknown", async () => {
|
| 605 |
+
host.harness.getConfig.mockResolvedValue({ defaultModel: "other/model", models: {} });
|
| 606 |
+
|
| 607 |
+
await bridge.handle(
|
| 608 |
+
{ id: "rpc-1", method: Methods.SaveConfig, params: { model: "custom/model", thinking: true, effort: "max" } },
|
| 609 |
+
"view-1",
|
| 610 |
+
);
|
| 611 |
+
|
| 612 |
+
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
| 613 |
+
defaultModel: "custom/model",
|
| 614 |
+
thinking: { enabled: true, effort: "max" },
|
| 615 |
+
});
|
| 616 |
+
});
|
| 617 |
+
|
| 618 |
+
it("leaves the stored effort alone when the pick re-confirms the active effort", async () => {
|
| 619 |
+
mockConfig({ enabled: false, effort: "low" });
|
| 620 |
+
|
| 621 |
+
await bridge.handle(
|
| 622 |
+
{ id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "high", effortChanged: false } },
|
| 623 |
+
"view-1",
|
| 624 |
+
);
|
| 625 |
+
|
| 626 |
+
expect(host.harness.setConfig).toHaveBeenCalledWith({
|
| 627 |
+
defaultModel: "kimi/reasoning",
|
| 628 |
+
thinking: { enabled: true },
|
| 629 |
+
});
|
| 630 |
+
});
|
| 631 |
+
|
| 632 |
+
it("skips the config write entirely when nothing changed", async () => {
|
| 633 |
+
mockConfig({ enabled: true, effort: "high" });
|
| 634 |
+
|
| 635 |
+
await bridge.handle(
|
| 636 |
+
{ id: "rpc-1", method: Methods.SaveConfig, params: { model: "kimi/reasoning", thinking: true, effort: "high" } },
|
| 637 |
+
"view-1",
|
| 638 |
+
);
|
| 639 |
+
|
| 640 |
+
expect(host.harness.setConfig).not.toHaveBeenCalled();
|
| 641 |
+
});
|
| 642 |
+
});
|
| 643 |
+
|
| 644 |
+
function createResumedSession(id: string, workDir: string) {
|
| 645 |
+
const close = vi.fn(async () => undefined);
|
| 646 |
+
const summary = {
|
| 647 |
+
id,
|
| 648 |
+
workDir,
|
| 649 |
+
sessionDir: join("/private/kimi/sessions", id),
|
| 650 |
+
createdAt: 1,
|
| 651 |
+
updatedAt: 2,
|
| 652 |
+
metadata: { vscode_legacy_approval: { yolo: false, afk: false } },
|
| 653 |
+
};
|
| 654 |
+
return {
|
| 655 |
+
id,
|
| 656 |
+
workDir,
|
| 657 |
+
summary,
|
| 658 |
+
close,
|
| 659 |
+
getResumeState: () => ({
|
| 660 |
+
sessionMetadata: { agents: {} },
|
| 661 |
+
agents: {
|
| 662 |
+
main: {
|
| 663 |
+
type: "main",
|
| 664 |
+
config: {
|
| 665 |
+
cwd: workDir,
|
| 666 |
+
modelAlias: "test-model",
|
| 667 |
+
modelCapabilities: {
|
| 668 |
+
image_in: false,
|
| 669 |
+
video_in: false,
|
| 670 |
+
audio_in: false,
|
| 671 |
+
thinking: false,
|
| 672 |
+
tool_use: true,
|
| 673 |
+
max_context_tokens: 128_000,
|
| 674 |
+
},
|
| 675 |
+
thinkingEffort: "off",
|
| 676 |
+
systemPrompt: "",
|
| 677 |
+
},
|
| 678 |
+
context: { history: [], tokenCount: 0 },
|
| 679 |
+
replay: [],
|
| 680 |
+
permission: { mode: "manual", rules: [] },
|
| 681 |
+
plan: null,
|
| 682 |
+
usage: {},
|
| 683 |
+
tools: [],
|
| 684 |
+
background: [],
|
| 685 |
+
},
|
| 686 |
+
},
|
| 687 |
+
}),
|
| 688 |
+
getStatus: async () => ({ permission: "manual" }),
|
| 689 |
+
setPermission: async () => undefined,
|
| 690 |
+
updateMetadata: async () => undefined,
|
| 691 |
+
setApprovalHandler: () => undefined,
|
| 692 |
+
setQuestionHandler: () => undefined,
|
| 693 |
+
onEvent: () => () => undefined,
|
| 694 |
+
};
|
| 695 |
+
}
|