kimi-code / apps /vscode /src /extension.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f59fbe2 verified
Raw
History Blame Contribute Delete
10.7 kB
import * as vscode from "vscode";
import { Events } from "../shared/bridge";
import { KimiWebviewProvider } from "./KimiWebviewProvider";
import { onSettingsChange, VSCodeSettings } from "./config/vscode-settings";
import {
LegacyMigrationManager,
type LegacyMigrationDiscovery,
type LegacyMigrationRunResult,
} from "./migration";
import { updateLoginContext } from "./utils/context";
let outputChannel: vscode.OutputChannel | undefined;
let provider: KimiWebviewProvider | undefined;
const LEGACY_REAUTH_NOTICE_KEY = "kimi.legacyMigration.reauthNotice.v1";
const LEGACY_WARNING_NOTICE_KEY = "kimi.legacyMigration.warningNotice.v1";
export async function activate(context: vscode.ExtensionContext): Promise<void> {
outputChannel = vscode.window.createOutputChannel("Kimi Code");
const remoteInfo = vscode.env.remoteName ? ` (remote: ${vscode.env.remoteName})` : "";
log(`Kimi Code ${VSCodeSettings.getExtensionConfig().version} activating${remoteInfo}`);
provider = new KimiWebviewProvider(
context.extensionUri,
context,
() => outputChannel?.show(),
(message) => log(message),
);
context.subscriptions.push(provider, outputChannel);
let isLoggedIn = false;
try {
isLoggedIn = await updateLoginContext(provider.harness);
} catch (error) {
logError("Unable to determine login status", error);
}
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider("kimi-baseline", {
provideTextDocumentContent: async (uri) => {
const sessionId = new URLSearchParams(uri.query).get("sessionId");
if (!sessionId || !provider) return "";
const relativePath = decodeURIComponent(uri.path.replace(/^\//, ""));
try {
return await provider.getBaselineContent(sessionId, relativePath);
} catch (error) {
logError("Unable to open baseline content", error);
return "";
}
},
}),
);
context.subscriptions.push(
onSettingsChange((changedKeys) => {
provider?.broadcast(Events.ExtensionConfigChanged, {
config: VSCodeSettings.getExtensionConfig(),
changedKeys,
});
if (changedKeys.includes("yoloMode")) {
void provider
?.setYoloModeForActiveSessions(VSCodeSettings.yoloMode)
.catch((error) => logError("Unable to update session permission", error));
}
}),
vscode.window.registerWebviewViewProvider("kimi.webview", provider, {
webviewOptions: { retainContextWhenHidden: true },
}),
);
const migrationManager = new LegacyMigrationManager({
targetHome: provider.harness.homeDir,
workspaceRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
legacyEnvironmentVariables: vscode.workspace
.getConfiguration("kimi")
.get<unknown>("environmentVariables"),
});
let migrationInFlight: Promise<void> | undefined;
const runMigration = (retry: boolean): Promise<void> => {
if (migrationInFlight !== undefined) return migrationInFlight;
migrationInFlight = performMigration(migrationManager, retry).finally(() => {
migrationInFlight = undefined;
});
return migrationInFlight;
};
const commands: Record<string, () => void | Promise<void>> = {
"kimi.clearAllState": async () => {
await context.globalState.update("kimi.config", undefined);
await context.globalState.update("kimi.mcpServers", undefined);
await context.workspaceState.update("kimi.mcpEnabled", undefined);
await vscode.window.showInformationMessage("Kimi: Extension UI state cleared.");
},
"kimi.openInTab": () => {
provider?.createPanel();
},
"kimi.openInSideBar": async () => {
await vscode.commands.executeCommand("kimi.webview.focus");
},
"kimi.focusInput": async () => {
await vscode.commands.executeCommand("kimi.webview.focus");
provider?.broadcast(Events.FocusInput, {});
},
"kimi.insertMention": async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
await vscode.window.showWarningMessage("No active editor");
return;
}
await vscode.commands.executeCommand("kimi.webview.focus");
if (!(await provider?.insertEditorMention(editor.document.uri, editor.selection))) {
await vscode.window.showWarningMessage("The active file is outside the selected working directory.");
}
},
"kimi.newConversation": async () => {
await vscode.commands.executeCommand("kimi.webview.focus");
provider?.broadcast(Events.NewConversation, {});
},
"kimi.showLogs": () => outputChannel?.show(),
"kimi.resetKimi": () => provider?.resetAllWebviews(),
"kimi.logout": async () => {
await vscode.commands.executeCommand("kimi.webview.focus");
await vscode.window.showInformationMessage("Use the logout button in Kimi settings.");
},
"kimi.migrateLegacyData": () => runMigration(true),
};
for (const [id, handler] of Object.entries(commands)) {
context.subscriptions.push(vscode.commands.registerCommand(id, handler));
}
void offerLegacyMigration(
migrationManager,
() => runMigration(false),
context.globalState,
isLoggedIn,
).catch((error) => {
logError("Unable to check for legacy Kimi data", error);
});
log("Kimi Code activated");
}
export async function deactivate(): Promise<void> {
log("Kimi Code deactivating");
await provider?.shutdown();
provider = undefined;
}
function log(message: string): void {
outputChannel?.appendLine(`[${new Date().toISOString()}] ${message}`);
}
function logError(message: string, error: unknown): void {
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
log(`${message}: ${detail}`);
}
async function offerLegacyMigration(
manager: LegacyMigrationManager,
migrate: () => Promise<void>,
globalState: vscode.Memento,
isLoggedIn: boolean,
): Promise<void> {
const discovery = await manager.discover();
logMigrationDiscovery(discovery);
const reauthNotice = legacyReauthNotice(discovery, isLoggedIn);
const warningNotice =
discovery.warnings.length === 0
? null
: "Some legacy Kimi data could not be inspected. Use “Kimi Code: Migrate Legacy Data” to retry.";
if (discovery.prompt === null) {
if (reauthNotice !== null && !globalState.get<boolean>(LEGACY_REAUTH_NOTICE_KEY, false)) {
await vscode.window.showWarningMessage(reauthNotice);
await globalState.update(LEGACY_REAUTH_NOTICE_KEY, true);
}
if (
discovery.warnings.length > 0 &&
!globalState.get<boolean>(LEGACY_WARNING_NOTICE_KEY, false)
) {
const action = await vscode.window.showWarningMessage(
warningNotice ?? "Some legacy Kimi data could not be inspected.",
"Show Logs",
);
await globalState.update(LEGACY_WARNING_NOTICE_KEY, true);
if (action === "Show Logs") outputChannel?.show();
}
return;
}
const action = await vscode.window.showInformationMessage(
[discovery.prompt.message, reauthNotice, warningNotice]
.filter((message) => message !== null)
.join(" "),
...discovery.prompt.actions.map(({ label }) => label),
);
if (reauthNotice !== null) await globalState.update(LEGACY_REAUTH_NOTICE_KEY, true);
if (warningNotice !== null) await globalState.update(LEGACY_WARNING_NOTICE_KEY, true);
if (action === "Migrate Now") await migrate();
}
function legacyReauthNotice(
discovery: LegacyMigrationDiscovery,
isLoggedIn: boolean,
): string | null {
const kimiLogins = isLoggedIn ? 0 : discovery.notices.oauthLoginsRequiringRelogin.length;
const mcpLogins = discovery.notices.mcpOauthServersRequiringReauth.length;
if (kimiLogins === 0 && mcpLogins === 0) return null;
if (kimiLogins > 0 && mcpLogins > 0) {
return "Legacy OAuth credentials are not copied. Sign in to Kimi Code and authorize your MCP servers again.";
}
return kimiLogins > 0
? "Legacy OAuth credentials are not copied. Sign in to Kimi Code again."
: "Legacy MCP OAuth credentials are not copied. Authorize those MCP servers again.";
}
async function performMigration(
manager: LegacyMigrationManager,
retry: boolean,
): Promise<void> {
log(`${retry ? "Retrying" : "Starting"} legacy Kimi data migration`);
const result = retry ? await manager.retry() : await manager.migrateNow();
logMigrationResult(result);
if (result.status === "completed" || result.status === "partial") {
try {
await provider?.harness.getConfig({ reload: true });
await provider?.resetAllWebviews();
} catch (error) {
logError("Migration finished, but the runtime config could not be reloaded", error);
}
}
const reauthCount =
result.notices.oauthLoginsRequiringRelogin.length +
result.notices.mcpOauthServersRequiringReauth.length;
const reauthNotice =
reauthCount === 0
? ""
: ` ${reauthCount} OAuth connection(s) must be signed in again.`;
const message = `${result.message}${reauthNotice}`;
const needsLogs =
result.status === "partial" ||
result.status === "failed" ||
result.warnings.length > 0 ||
result.manualActions.length > 0;
if (result.status === "failed") {
const action = await vscode.window.showErrorMessage(message, "Show Logs");
if (action === "Show Logs") outputChannel?.show();
} else if (needsLogs) {
const action = await vscode.window.showWarningMessage(message, "Show Logs");
if (action === "Show Logs") outputChannel?.show();
} else {
await vscode.window.showInformationMessage(message);
}
}
function logMigrationDiscovery(discovery: LegacyMigrationDiscovery): void {
for (const warning of discovery.warnings) log(`Legacy migration warning: ${warning.message}`);
for (const source of discovery.suppressedSources) {
log(`Legacy migration already completed for ${source.sourceHome}`);
}
}
function logMigrationResult(result: LegacyMigrationRunResult): void {
const { totals } = result;
log(
`Legacy migration ${result.status}: config=${totals.configFiles} mcp=${totals.mcpServers} history=${totals.userHistoryEntries} skills=${totals.skills} sessions=${totals.sessions} alreadyMigrated=${totals.alreadyMigratedSessions} skipped=${totals.skippedItems} conflicts=${totals.conflicts} failures=${totals.failures}`,
);
for (const warning of result.warnings) log(`Legacy migration warning: ${warning.message}`);
for (const source of result.sources) {
for (const failure of source.failures) {
log(`Legacy migration failure (${failure.sourceHome}): ${failure.message}`);
}
}
for (const action of result.manualActions) log(`Legacy migration action: ${action}`);
}
export { log };