Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/legacy-migration-targets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/migration-legacy": patch
"@moonshot-ai/kimi-code": patch
---

Keep legacy migrations idempotent across multiple Kimi homes and report damaged or unmapped sessions instead of silently skipping them.
7 changes: 7 additions & 0 deletions .changeset/vscode-node-sdk-host.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code-sdk": patch
"@moonshot-ai/kimi-code": patch
---

Support in-process editor hosts with session lifecycle, context, MCP configuration, and cross-platform session storage APIs.
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,48 @@ jobs:
- name: Smoke test CLI bundle
run: pnpm -C apps/kimi-code run smoke

vscode-vsix-package:
name: VSIX package audit (${{ matrix.target }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: all
- os: macos-latest
target: darwin-arm64
- os: windows-latest
target: win32-x64

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v6

- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm

- run: pnpm install --frozen-lockfile
- name: Build and audit target VSIX
run: pnpm --filter kimi-code run package:platform -- --target "${{ matrix.target }}"
- name: Run installed VSIX Extension Host smoke (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a pnpm --filter kimi-code run test:extension-host -- --version 1.100.0
- name: Run installed VSIX Extension Host smoke on stable (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a pnpm --filter kimi-code run test:extension-host -- --version stable
- name: Run installed VSIX Extension Host smoke
if: runner.os != 'Linux'
run: pnpm --filter kimi-code run test:extension-host -- --version 1.100.0
- uses: actions/upload-artifact@v4
with:
name: vscode-vsix-${{ matrix.target }}
path: apps/vscode/artifacts/vsix/*.vsix
if-no-files-found: error

test:
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -125,6 +167,8 @@ jobs:
echo "Typechecking ${config}"
pnpm dlx --package @typescript/native-preview@beta tsgo -p "${config}" --noEmit
done
- name: Typecheck VS Code extension
run: pnpm --filter kimi-code run typecheck
- name: Typecheck kimi-web (vue-tsc)
run: pnpm --filter @moonshot-ai/kimi-web run typecheck
- name: Typecheck vis-server
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ plugins/cdn/
.kimi-code/local.toml
.kimi-sandbox/
.vscode/
!apps/vscode/.vscode/
!apps/vscode/.vscode/*.json
apps/vscode/artifacts/

Dockerfile
docker-compose.yml
Expand Down
37 changes: 11 additions & 26 deletions apps/kimi-code/src/migration/detect-pending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
* shown. Cheap, synchronous-ish, no TTY required. Returns the MigrationPlan to
* drive the screen, or null when there is nothing to offer.
*/
import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { existsSync } from 'node:fs';

import { detectMigration, type MigrationPlan } from '@moonshot-ai/migration-legacy';
import {
detectMigration,
shouldSuppressMigration,
type MigrationPlan,
} from '@moonshot-ai/migration-legacy';

export interface DetectPendingInput {
readonly sourceHome: string;
Expand All @@ -24,11 +27,11 @@ export async function detectPendingMigration(
): Promise<MigrationPlan | null> {
const { sourceHome, targetHome } = input;
if (!existsSync(sourceHome)) return null;
if (input.ignoreMarker !== true) {
if (migrationAlreadyTargeted(join(sourceHome, '.migrated-to-kimi-code'), targetHome)) {
return null;
}
if (existsSync(join(targetHome, '.skip-migration-from-kimi-cli'))) return null;
if (
input.ignoreMarker !== true &&
shouldSuppressMigration({ sourceHome, targetHome })
) {
return null;
}

let plan: MigrationPlan;
Expand All @@ -52,21 +55,3 @@ export async function detectPendingMigration(

return plan;
}

/**
* True when the legacy `.migrated-to-kimi-code` marker records a migration
* into *this* target home. A marker written for a different `KIMI_CODE_HOME`
* must not suppress the prompt — that target has never received migrated data.
* An unreadable/old marker without `target_path` is treated as "matches"
* (conservative: do not re-prompt when the marker exists but is ambiguous).
*/
function migrationAlreadyTargeted(markerPath: string, targetHome: string): boolean {
if (!existsSync(markerPath)) return false;
try {
const parsed = JSON.parse(readFileSync(markerPath, 'utf-8')) as { target_path?: unknown };
if (typeof parsed.target_path !== 'string') return true;
return resolve(parsed.target_path) === resolve(targetHome);
} catch {
return true;
}
}
3 changes: 3 additions & 0 deletions apps/vis/server/src/lib/context-projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ export function projectContext(
}
break;
}
case 'context.update_token_count':
contextTokens = rec.tokenCount;
break;
case 'context.clear':
if (mode === 'model') {
messages = [];
Expand Down
10 changes: 10 additions & 0 deletions apps/vis/server/test/lib/context-projector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,16 @@ describe('context-projector', () => {
expect(proj.contextTokens).toBe(20); // 10+5+2+3, absolute (not summed across usage.record)
});

it('uses context.update_token_count as the latest absolute context-token snapshot', () => {
const entries = [
{ lineNo: 1, data: { type: 'context.update_token_count' as const, tokenCount: 42 }, raw: {} },
];

const proj = projectContext(entries as any);

expect(proj.contextTokens).toBe(42);
});

// ---- Fix ②: contextTokens updates on clear / compaction lifecycle events ---
// agent-core ContextMemory sets _tokenCount on clear() (→ 0) and
// applyCompaction(result) (→ result.tokensAfter), not only on step.end. These
Expand Down
6 changes: 6 additions & 0 deletions apps/vis/web/src/components/wire/renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ export const WIRE_RENDERERS: RendererMap = {
detail: (r) => <LoopEventDetail event={r.event} />,
},

'context.update_token_count': {
tone: 'meta',
label: 'tokens',
headline: (r) => ({ main: <Dim>context {r.tokenCount} tok</Dim> }),
},

'context.clear': {
tone: 'warning',
label: 'clear',
Expand Down
11 changes: 11 additions & 0 deletions apps/vis/web/src/lib/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,17 @@ export function analyzeWire(entries: readonly WireEntry[]): Analysis {
if (current) current.cancelled = true;
break;

case 'context.update_token_count':
contextTokens = rec.tokenCount;
contextSeries.push({
lineNo: entry.lineNo,
time: t,
turnIndex: current?.index ?? -1,
step: -1,
contextTokens,
});
if (contextTokens > peakContext) peakContext = contextTokens;
break;
case 'context.clear':
contextTokens = 0;
break;
Expand Down
11 changes: 11 additions & 0 deletions apps/vis/web/test/analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,15 @@ describe('analyzeWire', () => {
expect(a.contextSeries.map((p) => p.contextTokens)).toEqual([200, 200]);
expect(a.summary.peakContextTokens).toBe(200);
});

it('uses context.update_token_count as the absolute context-window fill', () => {
line = 0;
const a = analyzeWire([
e({ type: 'context.update_token_count', tokenCount: 42 }, 1),
]);

expect(a.summary.contextTokens).toBe(42);
expect(a.summary.peakContextTokens).toBe(42);
expect(a.contextSeries.map((point) => point.contextTokens)).toEqual([42]);
});
});
5 changes: 5 additions & 0 deletions apps/vscode/.vscode-test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from "@vscode/test-cli";

export default defineConfig({
files: "out/test/**/*.test.js",
});
29 changes: 29 additions & 0 deletions apps/vscode/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Kimi Code: Extension Development Host (isolated)",
"type": "extensionHost",
"request": "launch",
"preLaunchTask": "vscode: dev",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--user-data-dir=${workspaceFolder}/../../.tmp/vscode-extension-dev/user-data",
"--extensions-dir=${workspaceFolder}/../../.tmp/vscode-extension-dev/extensions",
"--disable-workspace-trust",
"--skip-welcome",
"--skip-release-notes",
"${workspaceFolder}/../../.tmp/vscode-extension-dev/workspace"
],
"env": {
"KIMI_CODE_HOME": "${workspaceFolder}/../../.tmp/vscode-extension-dev/kimi-home"
},
"outFiles": ["${workspaceFolder}/dist/**/*.js", "${workspaceFolder}/dist/**/*.mjs"],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
]
}
]
}
77 changes: 77 additions & 0 deletions apps/vscode/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "vscode: prepare isolated dev",
"type": "process",
"command": "pnpm",
"args": ["run", "dev:prepare"],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
},
{
"label": "vscode: watch extension",
"type": "process",
"command": "pnpm",
"args": ["run", "dev:extension"],
"options": {
"cwd": "${workspaceFolder}"
},
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"fileLocation": "absolute",
"pattern": {
"regexp": "^(.*?):(\\d+):(\\d+):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "Extension dev build complete; watching for changes"
}
}
},
{
"label": "vscode: watch webview",
"type": "process",
"command": "pnpm",
"args": ["run", "dev:webview"],
"options": {
"cwd": "${workspaceFolder}"
},
"isBackground": true,
"problemMatcher": {
"owner": "vite",
"fileLocation": "absolute",
"pattern": {
"regexp": "^(.*?):(\\d+):(\\d+):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "built in \\d+.*ms"
}
}
},
{
"label": "vscode: dev",
"dependsOrder": "parallel",
"dependsOn": [
"vscode: prepare isolated dev",
"vscode: watch extension",
"vscode: watch webview"
],
"problemMatcher": []
}
]
}
50 changes: 50 additions & 0 deletions apps/vscode/.vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Source files
src/**
webview-ui/**
shared/**
**/*.ts
**/*.tsx

# Node modules (bundled via esbuild/vite)
**/node_modules/**

# Build configs
.vscode/**
tsconfig.json
tsdown.config.ts
vitest.config.ts
.vscode-test.mjs
eslint.config.mjs
webview-ui/tsconfig.json
webview-ui/vite.config.ts
webview-ui/components.json

# Scripts
scripts/**
test/**
runtime/**
artifacts/**

# Never package generated test/debug state
**/.kimi/**
**/.kimi-code/**
**/cache/**
**/caches/**
**/credentials/**
**/logs/**
**/profile/**
**/profiles/**
**/session/**
**/sessions/**
**/tokens/**
**/*.jsonl
**/*.log

# Other
.gitignore
**/*.map
*.vsix
**/.DS_Store

# Prevent parent directory traversal via symlinks
../**
Loading
Loading