From f31b5295bcbc1bb85fd8f91beea580faf0dafe32 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 16 Jul 2026 18:02:50 +0800 Subject: [PATCH] feat: add the Kimi Code VS Code extension --- .changeset/web-workspace-hint.md | 5 + .github/workflows/vscode-extension.yml | 112 ++ .gitignore | 7 +- .vscode/launch.json | 17 + .vscode/tasks.json | 19 + AGENTS.md | 1 + apps/kimi-vscode/.vscodeignore | 6 + apps/kimi-vscode/README.md | 129 ++ apps/kimi-vscode/media/kimi.png | Bin 0 -> 82192 bytes apps/kimi-vscode/media/kimi.svg | 5 + apps/kimi-vscode/package.json | 72 ++ apps/kimi-vscode/scripts/prepare-runtime.mjs | 53 + apps/kimi-vscode/src/extension.ts | 570 +++++++++ apps/kimi-vscode/tsconfig.json | 7 + apps/kimi-vscode/tsdown.config.ts | 19 + apps/kimi-web/src/components/Sidebar.vue | 32 +- .../src/components/WorkspaceGroup.vue | 12 +- .../src/components/chat/ConversationPane.vue | 10 +- .../components/mobile/MobileSwitcherSheet.vue | 22 +- .../src/composables/client/useAppearance.ts | 21 +- .../composables/client/useWorkspaceState.ts | 39 +- .../src/composables/hostColorScheme.ts | 35 + apps/kimi-web/src/composables/useIsDark.ts | 6 +- .../src/composables/useKimiWebClient.ts | 35 +- apps/kimi-web/src/lib/workspaceHint.ts | 155 +++ apps/kimi-web/test/lib-logic.test.ts | 134 ++ apps/kimi-web/test/workspace-state.test.ts | 140 ++ flake.nix | 2 + package.json | 2 +- pnpm-lock.yaml | 1135 ++++++++++++++++- 30 files changed, 2753 insertions(+), 49 deletions(-) create mode 100644 .changeset/web-workspace-hint.md create mode 100644 .github/workflows/vscode-extension.yml create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 apps/kimi-vscode/.vscodeignore create mode 100644 apps/kimi-vscode/README.md create mode 100644 apps/kimi-vscode/media/kimi.png create mode 100644 apps/kimi-vscode/media/kimi.svg create mode 100644 apps/kimi-vscode/package.json create mode 100644 apps/kimi-vscode/scripts/prepare-runtime.mjs create mode 100644 apps/kimi-vscode/src/extension.ts create mode 100644 apps/kimi-vscode/tsconfig.json create mode 100644 apps/kimi-vscode/tsdown.config.ts create mode 100644 apps/kimi-web/src/composables/hostColorScheme.ts create mode 100644 apps/kimi-web/src/lib/workspaceHint.ts diff --git a/.changeset/web-workspace-hint.md b/.changeset/web-workspace-hint.md new file mode 100644 index 0000000000..08748a6375 --- /dev/null +++ b/.changeset/web-workspace-hint.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Support a `?workspace=` URL hint plus an ordered `&folder=` list so embedded hosts (the VS Code extension view) can pin and focus the UI on their own workspace folders, in folder order. A host-pushed color-scheme message makes 'system' theme mode follow the host's theme when embedded in VS Code. diff --git a/.github/workflows/vscode-extension.yml b/.github/workflows/vscode-extension.yml new file mode 100644 index 0000000000..a634db55d0 --- /dev/null +++ b/.github/workflows/vscode-extension.yml @@ -0,0 +1,112 @@ +name: vscode-extension + +# Builds the Kimi Code VS Code extension (apps/kimi-vscode) into a universal +# vsix (self-contained: CLI server bundle + web UI; node-pty natives are +# lifted from the user's own VS Code install at runtime, so no per-platform +# packages are needed). +# +# STATUS: temporarily MANUAL-ONLY — runs solely via workflow_dispatch from the +# Actions tab. Not wired into release.yml yet. To start publishing, set the +# repository secret VSCE_PAT (Azure DevOps PAT for the `moonshot-ai` +# publisher) and dispatch with `publish: true`; to automate later, add a +# release trigger here or call this workflow from release.yml +# (see desktop-build.yml's workflow_call pattern). +on: + workflow_dispatch: + inputs: + publish: + description: 'Publish the vsix to the marketplace (needs VSCE_PAT)' + required: false + type: boolean + default: false + retention-days: + description: 'Artifact retention in days' + required: false + type: number + default: 7 + +permissions: + contents: read + +jobs: + vsix: + name: Universal vsix + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build Kimi web assets + # The server serves apps/kimi-code/dist-web; it is staged into the + # extension's server/ runtime by prepare:server below. + run: | + pnpm --filter @moonshot-ai/kimi-web run build + node apps/kimi-code/scripts/copy-web-assets.mjs + + - name: Build the CLI server bundle + # Single-file JS bundle (dist-native/intermediates/main.cjs) — the + # same artifact the SEA build embeds, but here it runs under VS Code's + # own Electron Node. + run: pnpm --filter @moonshot-ai/kimi-code run build:native:js + + - name: Stage the extension server runtime + run: pnpm --filter kimi-vscode run prepare:server + + - name: Package the vsix + run: pnpm --filter kimi-vscode run package + + - name: Upload vsix + uses: actions/upload-artifact@v7 + with: + name: kimi-vscode-vsix + retention-days: ${{ inputs.retention-days }} + path: apps/kimi-vscode/artifacts/*.vsix + if-no-files-found: error + + publish: + name: Publish to marketplace + needs: vsix + if: inputs.publish + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'pnpm' + + - name: Install vsce only + run: pnpm install --frozen-lockfile --filter kimi-vscode + + - name: Download vsix + uses: actions/download-artifact@v6 + with: + name: kimi-vscode-vsix + path: apps/kimi-vscode/artifacts + + - name: Publish + shell: bash + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + # --skip-duplicate makes re-runs idempotent after a half-published + # state. When platform-specific packages ever become necessary, build + # them in a matrix and pass all vsix files to --packagePath at once. + run: pnpm --filter kimi-vscode exec vsce publish --packagePath apps/kimi-vscode/artifacts/*.vsix --skip-duplicate diff --git a/.gitignore b/.gitignore index 4bd389b824..d7ad80ece1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,12 @@ plugins/cdn/ .worktrees/ .kimi-code/local.toml .kimi-sandbox/ -.vscode/ +.vscode/* +!.vscode/launch.json +!.vscode/tasks.json +# kimi-vscode packaging artifacts (staged runtime + built vsix) +apps/kimi-vscode/server/ +apps/kimi-vscode/artifacts/ Dockerfile docker-compose.yml diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..fabcce9b5f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Kimi Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/apps/kimi-vscode", + "${workspaceFolder}" + ], + "outFiles": ["${workspaceFolder}/apps/kimi-vscode/dist/**/*.cjs"], + "preLaunchTask": "build: kimi-vscode", + "presentation": { "group": "kimi", "order": 1 } + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..7b07e0be58 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build: kimi-vscode", + "type": "shell", + "command": "pnpm -C apps/kimi-vscode run build", + "problemMatcher": [], + "presentation": { "group": "kimi" } + }, + { + "label": "watch: kimi-vscode", + "type": "shell", + "command": "pnpm -C apps/kimi-vscode exec tsdown --watch", + "isBackground": true, + "problemMatcher": [] + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 6891b73fd1..2f99c123de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo - `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`). - `apps/kimi-web`: the browser web UI, a peer to the TUI. Vue 3 + Vite + vue-i18n; talks to the server over REST + WebSocket under `/api/v1`. It must not depend on `@moonshot-ai/agent-core` (wire types are re-implemented locally). Debug against the two engines via the root `pnpm dev:v1` / `pnpm dev:v2` backend scripts — the dev Sidebar shows the active backend and switches it at runtime. See `apps/kimi-web/AGENTS.md`. +- `apps/kimi-vscode`: the VS Code extension. An activity-bar webview shell that iframes the server-served kimi-web UI (no UI code of its own, same shell-model as `apps/kimi-desktop`); spawns the local server from the monorepo sources in dev and registers every VS Code workspace folder (multi-root aware) via `POST /api/v1/workspaces`. The web UI's `?workspace=` / `&folder=` query params (`src/lib/workspaceHint.ts`) pin the initial selection and focus all workspace listings on those folders, in VS Code folder order. See `apps/kimi-vscode/README.md`. - `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays. - `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities. - `packages/node-sdk`: the public TypeScript SDK and harness. diff --git a/apps/kimi-vscode/.vscodeignore b/apps/kimi-vscode/.vscodeignore new file mode 100644 index 0000000000..04b313363c --- /dev/null +++ b/apps/kimi-vscode/.vscodeignore @@ -0,0 +1,6 @@ +**/*.map +src/** +scripts/** +tsconfig.json +tsdown.config.ts +.gitignore diff --git a/apps/kimi-vscode/README.md b/apps/kimi-vscode/README.md new file mode 100644 index 0000000000..599254e621 --- /dev/null +++ b/apps/kimi-vscode/README.md @@ -0,0 +1,129 @@ +# Kimi Code for VS Code + +A VS Code extension (workspace package `kimi-vscode`) that adds a +Kimi icon to the activity bar. Clicking it opens a sidebar webview that embeds +the existing web UI (`apps/kimi-web`) — same UI as the browser and the desktop +app, not a reimplementation. + +## How it works + +It follows the `apps/kimi-desktop` model (shell + process manager, no UI or +backend code of its own): + +1. On first open the extension starts the local Kimi server **in the + foreground** with `KIMI_CODE_EXPERIMENTAL_MULTI_SERVER=1` so it coexists + with a user's own daemon (its port falls through the kap-server `port + 1` + walk). What it spawns depends on how the extension itself was installed: + - **packaged (vsix)**: the CLI's single-file JS bundle (`server/main.cjs`) + running under VS Code's own Electron Node (`ELECTRON_RUN_AS_NODE=1` on + the extension host's `process.execPath` — the same switch VS Code's + `code` launcher uses). The bundle is staged into global storage together + with the embedded web UI and node-pty's native binaries, which are + **lifted from VS Code's own install** (`resources/app/node_modules/ + node-pty`, the copy the integrated terminal uses — so the ABI always + matches; lifting the native payloads of another app is the same trick + `p2p-live-share` uses). No platform binaries ship inside the extension. + - **dev (this monorepo)**: the apps/kimi-code sources via tsx (same spawn + line as the root `pnpm dev:kap-server` script, plus `--log-level error`), + serving `apps/kimi-code/dist-web`. +2. The actual origin and bearer token are parsed from the server's ready line + (`Kimi server: http://127.0.0.1:PORT/#token=`), then the extension + registers EVERY workspace folder (single- or multi-root) via + `POST /api/v1/workspaces`, in VS Code folder order (idempotent on root). +3. The webview is a one-file shell whose `${bridge}`, + ); +} + +/** Map VS Code's active color theme onto the web UI's light/dark scheme. */ +function hostColorScheme(): 'dark' | 'light' { + const kind = vscode.window.activeColorTheme.kind; + return kind === vscode.ColorThemeKind.Dark || kind === vscode.ColorThemeKind.HighContrast + ? 'dark' + : 'light'; +} + +/** Push the current VS Code theme scheme down to the webview shell (it + * forwards it into the iframe that hosts the web UI). */ +function postHostColorScheme(webview: vscode.Webview): void { + // VS Code's Webview.postMessage takes no target origin (not a DOM postMessage). + // oxlint-disable-next-line unicorn/require-post-message-target-origin + void webview.postMessage({ type: 'kimi-host-color-scheme', scheme: hostColorScheme() }); +} + +class KimiViewProvider implements vscode.WebviewViewProvider { + private view: vscode.WebviewView | undefined; + + constructor(private readonly manager: ServerManager) { + manager.onDidRestart(() => { + if (this.view !== undefined) void this.render(this.view); + }); + vscode.workspace.onDidChangeWorkspaceFolders(() => { + if (this.view !== undefined) void this.render(this.view); + }); + vscode.window.onDidChangeActiveColorTheme(() => { + if (this.view !== undefined) postHostColorScheme(this.view.webview); + }); + } + + resolveWebviewView(view: vscode.WebviewView): void { + this.view = view; + view.webview.options = { enableScripts: true }; + // The shell asks for the current scheme once loaded (covers reloads where + // an earlier push would have landed before its listener existed). + view.webview.onDidReceiveMessage((message: { type?: unknown }) => { + if (message.type === 'kimi-host-ready') postHostColorScheme(view.webview); + }); + void this.render(view); + } + + /** Render the view: start the server, register the workspace, iframe the UI. */ + private async render(view: vscode.WebviewView): Promise { + // All folders of the (possibly multi-root) workspace, in VS Code order; + // the first folder is the primary one. + const folders = (vscode.workspace.workspaceFolders ?? []).filter( + (folder) => folder.uri.scheme === 'file', + ); + if (folders.length === 0) { + view.webview.html = messageHtml('Open a folder to use Kimi Code.'); + return; + } + view.webview.html = messageHtml('Starting the Kimi server…'); + try { + const handle = await this.manager.ensure(); + // Idempotent on root — makes the VS Code folders the web UI's + // workspaces before the SPA first paints. + const roots = folders.map((folder) => folder.uri.fsPath); + for (const root of roots) { + await this.manager.registerWorkspace(handle, root); + } + view.webview.html = iframeHtml(handle, roots); + } catch (error) { + const text = error instanceof Error ? error.message : String(error); + this.manager.output.appendLine(`[kimi-vscode] render failed: ${text}`); + view.webview.html = messageHtml( + `Failed to start the Kimi server: ${escapeHtml(text)}
` + + `Run Kimi Code: Restart Server to retry.`, + ); + const choice = await vscode.window.showErrorMessage(`Kimi Code: ${text}`, 'Retry'); + if (choice === 'Retry') await vscode.commands.executeCommand('kimi-vscode.restart'); + } + } +} + +function escapeHtml(text: string): string { + return text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); +} + +// --------------------------------------------------------------------------- +// Activation +// --------------------------------------------------------------------------- + +let manager: ServerManager | undefined; + +export function activate(context: vscode.ExtensionContext): void { + manager = new ServerManager(context); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(VIEW_ID, new KimiViewProvider(manager), { + webviewOptions: { retainContextWhenHidden: true }, + }), + vscode.commands.registerCommand('kimi-vscode.restart', () => manager?.restart()), + manager, + ); +} + +export function deactivate(): void { + manager?.dispose(); + manager = undefined; +} diff --git a/apps/kimi-vscode/tsconfig.json b/apps/kimi-vscode/tsconfig.json new file mode 100644 index 0000000000..85792d9785 --- /dev/null +++ b/apps/kimi-vscode/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node", "vscode"] + }, + "include": ["src"] +} diff --git a/apps/kimi-vscode/tsdown.config.ts b/apps/kimi-vscode/tsdown.config.ts new file mode 100644 index 0000000000..ba9185950f --- /dev/null +++ b/apps/kimi-vscode/tsdown.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsdown'; + +// The extension host loads `./dist/extension.cjs` as CommonJS. All sources +// under src/ are bundled into a single file; `vscode` stays external (provided +// by VS Code) and Node built-ins are external by default. Mirrors +// apps/kimi-desktop's tsdown config (Electron main → out/main.cjs). +export default defineConfig({ + entry: { extension: 'src/extension.ts' }, + format: ['cjs'], + platform: 'node', + target: 'node20', + outDir: 'dist', + clean: true, + dts: false, + fixedExtension: true, + // Sourcemaps for F5 debugging of src/extension.ts (see .vscode/launch.json). + sourcemap: true, + deps: { neverBundle: ['vscode'] }, +}); diff --git a/apps/kimi-web/src/components/Sidebar.vue b/apps/kimi-web/src/components/Sidebar.vue index e422b22083..34e656d3c9 100644 --- a/apps/kimi-web/src/components/Sidebar.vue +++ b/apps/kimi-web/src/components/Sidebar.vue @@ -20,6 +20,7 @@ import { saveCollapsedWorkspaces, } from '../lib/storage'; import { moveInOrder, type DropPosition, type WorkspaceSortMode } from '../lib/workspaceOrder'; +import { hasSingleWorkspaceHostBinding, hasWorkspaceHostBinding } from '../lib/workspaceHint'; import type { Session, WorkspaceGroup as WorkspaceGroupType, WorkspaceView } from '../types'; import SearchSessionsDialog from './dialogs/SearchSessionsDialog.vue'; import WorkspaceGroup from './WorkspaceGroup.vue'; @@ -120,6 +121,16 @@ const emit = defineEmits<{ collapse: []; }>(); +// Host-provided workspace binding (lib/workspaceHint.ts): an embedded host +// (the VS Code extension view) fixes the workspace set, so remove-workspace +// entries stay hidden. With a SINGLE bound workspace the group header is +// redundant chrome — the sidebar renders that workspace's sessions flat, +// with no header to fold and no WORKSPACES section label. +const workspaceHostBound = hasWorkspaceHostBinding(); +const flatSingleWorkspace = computed( + () => hasSingleWorkspaceHostBinding() && props.groups.length === 1, +); + // --------------------------------------------------------------------------- // Session search dialog (Spotlight-style; filters title + last prompt) // --------------------------------------------------------------------------- @@ -168,6 +179,12 @@ function isCollapsed(id: string): boolean { return collapsedIds.value.has(id); } +// The collapse getter handed to WorkspaceGroup: in flat single-workspace mode +// the group header is hidden, so its sessions must always stay expanded. +function groupCollapsed(id: string): boolean { + return !flatSingleWorkspace.value && isCollapsed(id); +} + function toggleCollapse(id: string): void { const next = new Set(collapsedIds.value); if (next.has(id)) next.delete(id); @@ -722,7 +739,7 @@ onBeforeUnmount(() => {