diff --git a/bun.lock b/bun.lock index d830e9b4c..df5a8c25f 100644 --- a/bun.lock +++ b/bun.lock @@ -323,6 +323,10 @@ "packages/kilo-vscode": { "name": "kilo-code", "version": "0.0.1", + "dependencies": { + "eventsource": "^2.0.2", + "solid-js": "^1.9.11", + }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", @@ -330,6 +334,7 @@ "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "esbuild": "^0.27.2", + "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "npm-run-all": "^4.1.5", "typescript": "^5.9.3", @@ -2590,6 +2595,8 @@ "esbuild-plugin-copy": ["esbuild-plugin-copy@2.1.1", "", { "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", "fs-extra": "^10.0.1", "globby": "^11.0.3" }, "peerDependencies": { "esbuild": ">= 0.14.0" } }, "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw=="], + "esbuild-plugin-solid": ["esbuild-plugin-solid@0.6.0", "", { "dependencies": { "@babel/core": "^7.20.12", "@babel/preset-typescript": "^7.18.6", "babel-preset-solid": "^1.6.9" }, "peerDependencies": { "esbuild": ">=0.20", "solid-js": ">= 1.0" } }, "sha512-V1FvDALwLDX6K0XNYM9CMRAnMzA0+Ecu55qBUT9q/eAJh1KIDsTMFoOzMSgyHqbOfvrVfO3Mws3z7TW2GVnIZA=="], + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -2640,7 +2647,7 @@ "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "eventsource": ["eventsource@2.0.2", "", {}, "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -4460,6 +4467,8 @@ "@kilocode/kilo-gateway/@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="], + "@modelcontextprotocol/sdk/eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], @@ -4736,6 +4745,8 @@ "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "kilo-code/solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], + "kilo-code/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -5476,6 +5487,10 @@ "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "kilo-code/solid-js/seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], + + "kilo-code/solid-js/seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -5508,6 +5523,8 @@ "npm-run-all/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "opencontrol/@modelcontextprotocol/sdk/eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], "opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], diff --git a/packages/kilo-vscode/.gitignore b/packages/kilo-vscode/.gitignore new file mode 100644 index 000000000..e660fd93d --- /dev/null +++ b/packages/kilo-vscode/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/packages/kilo-vscode/.vscodeignore b/packages/kilo-vscode/.vscodeignore index 159277f02..dd93afca7 100644 --- a/packages/kilo-vscode/.vscodeignore +++ b/packages/kilo-vscode/.vscodeignore @@ -3,8 +3,10 @@ out/** node_modules/** src/** +webview-ui/** .gitignore .yarnrc +.npmrc esbuild.js vsc-extension-quickstart.md **/tsconfig.json @@ -12,3 +14,7 @@ vsc-extension-quickstart.md **/*.map **/*.ts **/.vscode-test.* +pnpm-lock.yaml + +# Include bin/ directory for CLI binary (production) +!bin/** diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 16fe419c3..b59f3a24f 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -99,11 +99,12 @@ }, "scripts": { "vscode:prepublish": "pnpm run package", - "compile": "pnpm run check-types && pnpm run lint && node esbuild.js", + "prepare:cli-binary": "node scripts/prepare-cli-binary.mjs", + "compile": "pnpm run prepare:cli-binary && pnpm run check-types && pnpm run lint && node esbuild.js", "watch": "npm-run-all -p watch:*", - "watch:esbuild": "node esbuild.js --watch", + "watch:esbuild": "pnpm run prepare:cli-binary && node esbuild.js --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", - "package": "pnpm run check-types && pnpm run lint && node esbuild.js --production", + "package": "pnpm run prepare:cli-binary && pnpm run check-types && pnpm run lint && node esbuild.js --production", "compile-tests": "tsc -p . --outDir out", "watch-tests": "tsc -p . -w --outDir out", "pretest": "pnpm run compile-tests && pnpm run compile && pnpm run lint", @@ -125,6 +126,7 @@ "typescript-eslint": "^8.54.0" }, "dependencies": { + "eventsource": "^2.0.2", "solid-js": "^1.9.11" } } diff --git a/packages/kilo-vscode/pnpm-lock.yaml b/packages/kilo-vscode/pnpm-lock.yaml index 7d3cfe784..d4d0700f9 100644 --- a/packages/kilo-vscode/pnpm-lock.yaml +++ b/packages/kilo-vscode/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + eventsource: + specifier: ^2.0.2 + version: 2.0.2 solid-js: specifier: ^1.9.11 version: 1.9.11 @@ -869,6 +872,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1858,7 +1865,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2018,7 +2025,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -2558,10 +2565,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@4.4.3: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -2802,6 +2805,8 @@ snapshots: esutils@2.0.3: {} + eventsource@2.0.2: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} diff --git a/packages/kilo-vscode/scripts/prepare-cli-binary.mjs b/packages/kilo-vscode/scripts/prepare-cli-binary.mjs new file mode 100644 index 000000000..adff96a08 --- /dev/null +++ b/packages/kilo-vscode/scripts/prepare-cli-binary.mjs @@ -0,0 +1,120 @@ +import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, chmodSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { execSync } from "node:child_process"; + +/** + * Ensures the VS Code extension has a CLI binary at `packages/kilo-vscode/bin/kilo`. + * + * Strategy: + * 1) If `bin/kilo` already exists -> ok. + * 2) Else try to locate a prebuilt binary produced by `packages/opencode` build. + * 3) Else try to build it via `bun run build --single` in `packages/opencode`. + * 4) Copy the resulting binary into `packages/kilo-vscode/bin/kilo` and chmod +x. + * + * This script is intended to be run from `packages/kilo-vscode` as part of build/package. + */ + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const kiloVscodeDir = path.resolve(__dirname, ".."); +const repoRootDir = path.resolve(kiloVscodeDir, ".."); +const opencodeDir = path.resolve(repoRootDir, "opencode"); + +const targetBinDir = path.resolve(kiloVscodeDir, "bin"); +const targetBinPath = path.resolve(targetBinDir, "kilo"); + +function log(msg) { + // Keep logs machine-grep friendly in CI + console.log(`[prepare-cli-binary] ${msg}`); +} + +function findKiloBinaryInOpencodeDist() { + const distDir = path.resolve(opencodeDir, "dist"); + if (!existsSync(distDir)) return null; + + // Expected: packages/opencode/dist/@kilocode/cli-/bin/kilo + // But keep it flexible: find any dist/**/bin/kilo + /** @type {string[]} */ + const queue = [distDir]; + while (queue.length) { + const dir = queue.pop(); + if (!dir) continue; + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + continue; + } + for (const e of entries) { + const p = path.join(dir, e.name); + if (e.isDirectory()) { + queue.push(p); + continue; + } + if (e.isFile() && e.name === "kilo" && path.basename(path.dirname(p)) === "bin") { + return p; + } + } + } + return null; +} + +function ensureBuiltBinary() { + const found = findKiloBinaryInOpencodeDist(); + if (found) return found; + + log(`No prebuilt binary found under ${path.relative(kiloVscodeDir, path.resolve(opencodeDir, "dist"))} - attempting build via bun.`); + + try { + execSync("bun --version", { stdio: "ignore" }); + } catch { + throw new Error( + `Bun is required to build the CLI binary, but was not found on PATH. ` + + `Install bun, or build the CLI separately in ${opencodeDir} and re-run.` + ); + } + + // Build using the opencode package script. + execSync("bun run build --single", { + cwd: opencodeDir, + stdio: "inherit", + env: process.env, + }); + + const built = findKiloBinaryInOpencodeDist(); + if (!built) { + throw new Error( + `CLI build completed but no binary was found in ${path.resolve(opencodeDir, "dist")} (expected dist/**/bin/kilo).` + ); + } + return built; +} + +function main() { + if (existsSync(targetBinPath)) { + const st = statSync(targetBinPath); + log(`CLI binary already present at ${path.relative(kiloVscodeDir, targetBinPath)} (${Math.round(st.size / 1024 / 1024)}MB).`); + return; + } + + if (!existsSync(opencodeDir)) { + throw new Error(`Expected opencode package at ${opencodeDir}, but it does not exist.`); + } + + const sourceBinPath = ensureBuiltBinary(); + mkdirSync(targetBinDir, { recursive: true }); + copyFileSync(sourceBinPath, targetBinPath); + chmodSync(targetBinPath, 0o755); + + log(`Copied CLI binary from ${path.relative(repoRootDir, sourceBinPath)} -> ${path.relative(kiloVscodeDir, targetBinPath)}`); +} + +try { + main(); +} catch (err) { + console.error(`[prepare-cli-binary] ERROR: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); +} + diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 2a1e6d86b..b95679d03 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1,36 +1,279 @@ import * as vscode from 'vscode'; +import { + ServerManager, + HttpClient, + SSEClient, + type SessionInfo, + type SSEEvent, + type ServerConfig, +} from './services/cli-backend'; export class KiloProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'kilo-code.new.sidebarView'; - private _view?: vscode.WebviewView; + private readonly serverManager: ServerManager; + private httpClient: HttpClient | null = null; + private sseClient: SSEClient | null = null; + private webviewView: vscode.WebviewView | null = null; + private currentSession: SessionInfo | null = null; - constructor(private readonly _extensionUri: vscode.Uri) {} + constructor( + private readonly extensionUri: vscode.Uri, + context: vscode.ExtensionContext + ) { + this.serverManager = new ServerManager(context); + } public resolveWebviewView( webviewView: vscode.WebviewView, _context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken ) { - this._view = webviewView; + // Store the webview reference + this.webviewView = webviewView; + // Set up webview options webviewView.webview.options = { enableScripts: true, - localResourceRoots: [this._extensionUri] + localResourceRoots: [this.extensionUri] }; + // Set HTML content webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + + // Handle messages from webview + webviewView.webview.onDidReceiveMessage(async (message) => { + switch (message.type) { + case 'sendMessage': + await this.handleSendMessage(message.text); + break; + case 'abort': + await this.handleAbort(); + break; + case 'permissionResponse': + await this.handlePermissionResponse( + message.permissionId, + message.response + ); + break; + } + }); + + // Initialize connection to CLI backend + this.initializeConnection(); + } + + /** + * Initialize connection to the CLI backend server. + */ + private async initializeConnection(): Promise { + console.log('[Kilo New] KiloProvider: ๐Ÿ”ง Starting initializeConnection...'); + try { + // Get server from server manager + console.log('[Kilo New] KiloProvider: ๐Ÿ“ก Requesting server from serverManager...'); + const server = await this.serverManager.getServer(); + console.log('[Kilo New] KiloProvider: โœ… Server obtained:', { port: server.port, hasPassword: !!server.password }); + + // Create config with baseUrl and password + const config: ServerConfig = { + baseUrl: `http://127.0.0.1:${server.port}`, + password: server.password, + }; + console.log('[Kilo New] KiloProvider: ๐Ÿ”‘ Created config:', { baseUrl: config.baseUrl }); + + // Create HttpClient and SSEClient instances + this.httpClient = new HttpClient(config); + this.sseClient = new SSEClient(config); + console.log('[Kilo New] KiloProvider: ๐Ÿ”Œ Created HttpClient and SSEClient'); + + // Set up SSE event handling + this.sseClient.onEvent((event) => { + console.log('[Kilo New] KiloProvider: ๐Ÿ“จ Received SSE event:', event.type); + this.handleSSEEvent(event); + }); + + this.sseClient.onStateChange((state) => { + console.log('[Kilo New] KiloProvider: ๐Ÿ”„ SSE state changed to:', state); + console.log('[Kilo New] KiloProvider: ๐Ÿ“ค Posting connectionState message to webview:', state); + this.postMessage({ + type: 'connectionState', + state, + }); + }); + + // Connect SSE with workspace directory + const workspaceDir = this.getWorkspaceDirectory(); + console.log('[Kilo New] KiloProvider: ๐Ÿ“‚ Connecting SSE with workspace:', workspaceDir); + this.sseClient.connect(workspaceDir); + + // Post "ready" message to webview with server info + console.log('[Kilo New] KiloProvider: ๐Ÿ“ค Posting ready message to webview'); + this.postMessage({ + type: 'ready', + serverInfo: { + port: server.port, + }, + }); + console.log('[Kilo New] KiloProvider: โœ… initializeConnection completed successfully'); + } catch (error) { + console.error('[Kilo New] KiloProvider: โŒ Failed to initialize connection:', error); + this.postMessage({ + type: 'error', + message: error instanceof Error ? error.message : 'Failed to connect to CLI backend', + }); + } } - public postMessage(message: unknown) { - if (this._view) { - this._view.webview.postMessage(message); + /** + * Handle sending a message from the webview. + */ + private async handleSendMessage(text: string): Promise { + if (!this.httpClient) { + this.postMessage({ + type: 'error', + message: 'Not connected to CLI backend', + }); + return; + } + + try { + const workspaceDir = this.getWorkspaceDirectory(); + + // Create session if needed + if (!this.currentSession) { + this.currentSession = await this.httpClient.createSession(workspaceDir); + } + + // Send message with text part + await this.httpClient.sendMessage( + this.currentSession.id, + [{ type: 'text', text }], + workspaceDir + ); + } catch (error) { + console.error('[Kilo New] KiloProvider: Failed to send message:', error); + this.postMessage({ + type: 'error', + message: error instanceof Error ? error.message : 'Failed to send message', + }); + } + } + + /** + * Handle abort request from the webview. + */ + private async handleAbort(): Promise { + if (!this.httpClient || !this.currentSession) { + return; + } + + try { + const workspaceDir = this.getWorkspaceDirectory(); + await this.httpClient.abortSession(this.currentSession.id, workspaceDir); + } catch (error) { + console.error('[Kilo New] KiloProvider: Failed to abort session:', error); } } + /** + * Handle permission response from the webview. + */ + private async handlePermissionResponse( + permissionId: string, + response: 'once' | 'always' | 'reject' + ): Promise { + if (!this.httpClient || !this.currentSession) { + return; + } + + try { + const workspaceDir = this.getWorkspaceDirectory(); + await this.httpClient.respondToPermission( + this.currentSession.id, + permissionId, + response, + workspaceDir + ); + } catch (error) { + console.error('[Kilo New] KiloProvider: Failed to respond to permission:', error); + } + } + + /** + * Handle SSE events from the CLI backend. + */ + private handleSSEEvent(event: SSEEvent): void { + // Filter events by sessionID (only process events for current session) + if ('sessionID' in event.properties) { + if (this.currentSession && event.properties.sessionID !== this.currentSession.id) { + return; + } + } + + // Forward relevant events to webview + switch (event.type) { + case 'message.part.updated': + this.postMessage({ + type: 'partUpdated', + part: event.properties.part, + delta: event.properties.delta, + }); + break; + + case 'session.status': + this.postMessage({ + type: 'sessionStatus', + sessionID: event.properties.sessionID, + status: event.properties.status, + }); + break; + + case 'permission.asked': + this.postMessage({ + type: 'permissionRequest', + ...event.properties, + }); + break; + + case 'todo.updated': + this.postMessage({ + type: 'todoUpdated', + sessionID: event.properties.sessionID, + items: event.properties.items, + }); + break; + + case 'session.created': + // Store session if we don't have one yet + if (!this.currentSession) { + this.currentSession = event.properties.info; + } + break; + } + } + + /** + * Post a message to the webview. + * Public so toolbar button commands can send messages. + */ + public postMessage(message: unknown): void { + this.webviewView?.webview.postMessage(message); + } + + /** + * Get the workspace directory. + */ + private getWorkspaceDirectory(): string { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + return workspaceFolders[0].uri.fsPath; + } + return process.cwd(); + } + private _getHtmlForWebview(webview: vscode.Webview): string { const scriptUri = webview.asWebviewUri( - vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview.js') + vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview.js') ); const nonce = getNonce(); @@ -67,6 +310,14 @@ export class KiloProvider implements vscode.WebviewViewProvider { `; } + + /** + * Dispose of the provider and clean up resources. + */ + dispose(): void { + this.sseClient?.dispose(); + this.serverManager.dispose(); + } } function getNonce(): string { diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 8d430f410..4965cd140 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -5,8 +5,10 @@ import { AgentManagerProvider } from './AgentManagerProvider'; export function activate(context: vscode.ExtensionContext) { console.log('Kilo Code extension is now active'); + // Create the provider with extensionUri and context + const provider = new KiloProvider(context.extensionUri, context); + // Register the webview view provider for the sidebar - const provider = new KiloProvider(context.extensionUri); context.subscriptions.push( vscode.window.registerWebviewViewProvider(KiloProvider.viewType, provider) ); @@ -36,6 +38,11 @@ export function activate(context: vscode.ExtensionContext) { provider.postMessage({ type: 'action', action: 'settingsButtonClicked' }); }) ); + + // Add dispose handler to subscriptions + context.subscriptions.push({ + dispose: () => provider.dispose() + }); } export function deactivate() {} diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts new file mode 100644 index 000000000..db110d5b5 --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -0,0 +1,159 @@ +import type { ServerConfig, SessionInfo, MessageInfo, MessagePart } from "./types" + +/** + * HTTP Client for communicating with the CLI backend server. + * Handles all REST API calls for session management, messaging, and permissions. + */ +export class HttpClient { + private readonly baseUrl: string + private readonly authHeader: string + private readonly authUsername = "opencode" + + constructor(config: ServerConfig) { + this.baseUrl = config.baseUrl + // Auth header format: Basic base64("opencode:password") + // NOTE: The CLI server expects a non-empty username ("opencode"). Using an empty username + // (":password") results in 401 for both REST and SSE endpoints. + this.authHeader = `Basic ${Buffer.from(`${this.authUsername}:${config.password}`).toString("base64")}` + + // Safe debug logging: no secrets. + console.log("[Kilo New] HTTP: ๐Ÿ” Auth configured", { + username: this.authUsername, + passwordLength: config.password.length, + }) + } + + /** + * Make an HTTP request to the CLI backend server. + */ + private async request( + method: string, + path: string, + body?: unknown, + options?: { directory?: string } + ): Promise { + const url = `${this.baseUrl}${path}` + + const headers: Record = { + Authorization: this.authHeader, + "Content-Type": "application/json", + } + + if (options?.directory) { + headers["x-opencode-directory"] = options.directory + } + + const response = await fetch(url, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }) + + if (!response.ok) { + let errorMessage: string + try { + const errorJson = (await response.json()) as { error?: string; message?: string } + errorMessage = errorJson.error || errorJson.message || response.statusText + } catch { + errorMessage = response.statusText + } + throw new Error(`HTTP ${response.status}: ${errorMessage}`) + } + + return response.json() as Promise + } + + // ============================================ + // Session Management Methods + // ============================================ + + /** + * Create a new session in the specified directory. + */ + async createSession(directory: string): Promise { + return this.request("POST", "/session", {}, { directory }) + } + + /** + * Get information about an existing session. + */ + async getSession(sessionId: string, directory: string): Promise { + return this.request("GET", `/session/${sessionId}`, undefined, { directory }) + } + + /** + * List all sessions in the specified directory. + */ + async listSessions(directory: string): Promise { + return this.request("GET", "/session", undefined, { directory }) + } + + // ============================================ + // Messaging Methods + // ============================================ + + /** + * Send a message to a session. + */ + async sendMessage( + sessionId: string, + parts: Array<{ type: "text"; text: string } | { type: "file"; mime: string; url: string }>, + directory: string + ): Promise<{ info: MessageInfo; parts: MessagePart[] }> { + return this.request<{ info: MessageInfo; parts: MessagePart[] }>( + "POST", + `/session/${sessionId}/message`, + { parts }, + { directory } + ) + } + + /** + * Get all messages for a session. + */ + async getMessages( + sessionId: string, + directory: string + ): Promise> { + return this.request>( + "GET", + `/session/${sessionId}/message`, + undefined, + { directory } + ) + } + + // ============================================ + // Control Methods + // ============================================ + + /** + * Abort the current operation in a session. + */ + async abortSession(sessionId: string, directory: string): Promise { + await this.request("POST", `/session/${sessionId}/abort`, {}, { directory }) + return true + } + + // ============================================ + // Permission Methods + // ============================================ + + /** + * Respond to a permission request. + */ + async respondToPermission( + sessionId: string, + permissionId: string, + response: "once" | "always" | "reject", + directory: string + ): Promise { + await this.request( + "POST", + `/session/${sessionId}/permissions/${permissionId}`, + { response }, + { directory } + ) + return true + } +} diff --git a/packages/kilo-vscode/src/services/cli-backend/index.ts b/packages/kilo-vscode/src/services/cli-backend/index.ts new file mode 100644 index 000000000..75a17a818 --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/index.ts @@ -0,0 +1,22 @@ +// Main exports for cli-backend services +// Classes will be exported here as they are created in subsequent phases + +export type { + SessionInfo, + SessionStatusInfo, + MessageInfo, + MessagePart, + ToolState, + PermissionRequest, + SSEEvent, + TodoItem, + ServerConfig, +} from "./types" + +export { ServerManager } from "./server-manager" +export type { ServerInstance } from "./server-manager" + +export { HttpClient } from "./http-client" + +export { SSEClient } from "./sse-client" +export type { SSEEventHandler, SSEErrorHandler, SSEStateHandler } from "./sse-client" diff --git a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts new file mode 100644 index 000000000..47f503724 --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts @@ -0,0 +1,134 @@ +import { spawn, ChildProcess } from "child_process" +import * as crypto from "crypto" +import * as fs from "fs" +import * as path from "path" +import * as vscode from "vscode" + +export interface ServerInstance { + port: number + password: string + process: ChildProcess +} + +export class ServerManager { + private instance: ServerInstance | null = null + private startupPromise: Promise | null = null + + constructor(private readonly context: vscode.ExtensionContext) {} + + /** + * Get or start the server instance + */ + async getServer(): Promise { + console.log('[Kilo New] ServerManager: ๐Ÿ” getServer called'); + if (this.instance) { + console.log('[Kilo New] ServerManager: โ™ป๏ธ Returning existing instance:', { port: this.instance.port }); + return this.instance + } + + if (this.startupPromise) { + console.log('[Kilo New] ServerManager: โณ Startup already in progress, waiting...'); + return this.startupPromise + } + + console.log('[Kilo New] ServerManager: ๐Ÿš€ Starting new server instance...'); + this.startupPromise = this.startServer() + try { + this.instance = await this.startupPromise + console.log('[Kilo New] ServerManager: โœ… Server started successfully:', { port: this.instance.port }); + return this.instance + } finally { + this.startupPromise = null + } + } + + private async startServer(): Promise { + const password = crypto.randomBytes(32).toString("hex") + const cliPath = this.getCliPath() + console.log('[Kilo New] ServerManager: ๐Ÿ“ CLI path:', cliPath); + console.log('[Kilo New] ServerManager: ๐Ÿ” Generated password (length):', password.length); + + // Verify the CLI binary exists + if (!fs.existsSync(cliPath)) { + throw new Error(`CLI binary not found at expected path: ${cliPath}. Please ensure the CLI is built and bundled with the extension.`) + } + + const stat = fs.statSync(cliPath) + console.log('[Kilo New] ServerManager: ๐Ÿ“„ CLI isFile:', stat.isFile()); + console.log('[Kilo New] ServerManager: ๐Ÿ“„ CLI mode (octal):', (stat.mode & 0o777).toString(8)); + + return new Promise((resolve, reject) => { + console.log('[Kilo New] ServerManager: ๐ŸŽฌ Spawning CLI process:', cliPath, ['serve', '--port', '0']); + const serverProcess = spawn(cliPath, ["serve", "--port", "0"], { + env: { + ...process.env, + OPENCODE_SERVER_PASSWORD: password, + OPENCODE_CLIENT: "vscode", + }, + stdio: ["ignore", "pipe", "pipe"], + }) + console.log('[Kilo New] ServerManager: ๐Ÿ“ฆ Process spawned with PID:', serverProcess.pid); + + let resolved = false + + serverProcess.stdout?.on("data", (data: Buffer) => { + const output = data.toString() + console.log("[Kilo New] ServerManager: ๐Ÿ“ฅ CLI Server stdout:", output) + + // Parse: "kilo server listening on http://127.0.0.1:12345" + const match = output.match(/listening on http:\/\/[\w.]+:(\d+)/) + if (match && !resolved) { + resolved = true + const port = parseInt(match[1], 10) + console.log('[Kilo New] ServerManager: ๐ŸŽฏ Port detected:', port); + resolve({ port, password, process: serverProcess }) + } + }) + + serverProcess.stderr?.on("data", (data: Buffer) => { + const errorOutput = data.toString() + console.error("[Kilo New] ServerManager: โš ๏ธ CLI Server stderr:", errorOutput) + }) + + serverProcess.on("error", (error) => { + console.error('[Kilo New] ServerManager: โŒ Process error:', error); + if (!resolved) { + reject(error) + } + }) + + serverProcess.on("exit", (code) => { + console.log("[Kilo New] ServerManager: ๐Ÿ›‘ Process exited with code:", code) + if (this.instance?.process === serverProcess) { + this.instance = null + } + if (!resolved) { + reject(new Error(`CLI process exited with code ${code} before server started`)) + } + }) + + // Timeout after 30 seconds + setTimeout(() => { + if (!resolved) { + console.error('[Kilo New] ServerManager: โฐ Server startup timeout (30s)'); + serverProcess.kill() + reject(new Error("Server startup timeout")) + } + }, 30000) + }) + } + + private getCliPath(): string { + // Always use the bundled binary from the extension directory + const cliPath = path.join(this.context.extensionPath, "bin", "kilo") + console.log('[Kilo New] ServerManager: ๐Ÿ“ฆ Using CLI path:', cliPath); + return cliPath + } + + dispose(): void { + if (this.instance) { + this.instance.process.kill("SIGTERM") + this.instance = null + } + } +} diff --git a/packages/kilo-vscode/src/services/cli-backend/sse-client.ts b/packages/kilo-vscode/src/services/cli-backend/sse-client.ts new file mode 100644 index 000000000..b662b22a5 --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/sse-client.ts @@ -0,0 +1,180 @@ +import EventSource from "eventsource" +import type { ServerConfig, SSEEvent } from "./types" + +// Type definitions for handlers +export type SSEEventHandler = (event: SSEEvent) => void +export type SSEErrorHandler = (error: Error) => void +export type SSEStateHandler = (state: "connecting" | "connected" | "disconnected") => void + +/** + * SSE Client for receiving real-time events from the CLI backend. + * Manages EventSource connection and distributes events to subscribers. + */ +export class SSEClient { + private eventSource: EventSource | null = null + private handlers: Set = new Set() + private errorHandlers: Set = new Set() + private stateHandlers: Set = new Set() + private readonly authUsername = "opencode" + + constructor(private readonly config: ServerConfig) {} + + /** + * Connect to the SSE endpoint for a specific directory. + * @param directory - The workspace directory to subscribe to events for + */ + connect(directory: string): void { + console.log('[Kilo New] SSE: ๐Ÿ”Œ connect() called with directory:', directory); + + // Return early if already connected + if (this.eventSource) { + console.log('[Kilo New] SSE: โš ๏ธ Already connected, skipping'); + return + } + + // Notify connecting state + console.log('[Kilo New] SSE: ๐Ÿ”„ Setting state to "connecting"'); + this.notifyState("connecting") + + // Build URL with directory parameter + const url = `${this.config.baseUrl}/event?directory=${encodeURIComponent(directory)}` + console.log('[Kilo New] SSE: ๐ŸŒ Connecting to URL:', url); + + // Create auth header + const authHeader = `Basic ${Buffer.from(`${this.authUsername}:${this.config.password}`).toString("base64")}` + console.log('[Kilo New] SSE: ๐Ÿ”‘ Auth header created', { + username: this.authUsername, + passwordLength: this.config.password.length, + }); + + // Create EventSource with headers + console.log('[Kilo New] SSE: ๐ŸŽฌ Creating EventSource...'); + this.eventSource = new EventSource(url, { + headers: { + Authorization: authHeader, + }, + }) + + // Set up onopen handler + this.eventSource.onopen = () => { + console.log('[Kilo New] SSE: โœ… EventSource opened successfully'); + this.notifyState("connected") + } + + // Set up onmessage handler + this.eventSource.onmessage = (messageEvent) => { + console.log('[Kilo New] SSE: ๐Ÿ“จ Received message event:', messageEvent.data); + try { + const event = JSON.parse(messageEvent.data) as SSEEvent + console.log('[Kilo New] SSE: ๐Ÿ“ฆ Parsed event type:', event.type); + this.notifyEvent(event) + } catch (error) { + console.error("[Kilo New] SSE: โŒ Failed to parse event:", error) + this.notifyError(error instanceof Error ? error : new Error(String(error))) + } + } + + // Set up onerror handler + this.eventSource.onerror = (errorEvent) => { + console.error("[Kilo New] SSE: โŒ EventSource error:", errorEvent) + this.notifyError(new Error("EventSource connection error")) + this.notifyState("disconnected") + } + } + + /** + * Disconnect from the SSE endpoint. + */ + disconnect(): void { + if (this.eventSource) { + this.eventSource.close() + this.eventSource = null + } + this.notifyState("disconnected") + } + + /** + * Subscribe to SSE events. + * @param handler - Function to call when an event is received + * @returns Unsubscribe function + */ + onEvent(handler: SSEEventHandler): () => void { + this.handlers.add(handler) + return () => { + this.handlers.delete(handler) + } + } + + /** + * Subscribe to error events. + * @param handler - Function to call when an error occurs + * @returns Unsubscribe function + */ + onError(handler: SSEErrorHandler): () => void { + this.errorHandlers.add(handler) + return () => { + this.errorHandlers.delete(handler) + } + } + + /** + * Subscribe to connection state changes. + * @param handler - Function to call when state changes + * @returns Unsubscribe function + */ + onStateChange(handler: SSEStateHandler): () => void { + this.stateHandlers.add(handler) + return () => { + this.stateHandlers.delete(handler) + } + } + + /** + * Notify all event handlers of a new event. + */ + private notifyEvent(event: SSEEvent): void { + for (const handler of this.handlers) { + try { + handler(event) + } catch (error) { + console.error("[Kilo New] SSE: Error in event handler:", error) + } + } + } + + /** + * Notify all error handlers of an error. + */ + private notifyError(error: Error): void { + for (const handler of this.errorHandlers) { + try { + handler(error) + } catch (err) { + console.error("[Kilo New] SSE: Error in error handler:", err) + } + } + } + + /** + * Notify all state handlers of a state change. + */ + private notifyState(state: "connecting" | "connected" | "disconnected"): void { + for (const handler of this.stateHandlers) { + try { + handler(state) + } catch (error) { + console.error("[Kilo New] SSE: Error in state handler:", error) + } + } + } + + /** + * Dispose of the client, disconnecting and clearing all handlers. + */ + dispose(): void { + this.disconnect() + this.handlers.clear() + this.errorHandlers.clear() + this.stateHandlers.clear() + } +} diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts new file mode 100644 index 000000000..cdeac2107 --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -0,0 +1,85 @@ +// Session types from @kilocode/cli +export interface SessionInfo { + id: string + title: string + directory: string + parentID?: string + share?: string + time: { + created: number + updated: number + archived?: number + } +} + +// Session status from SessionStatus.Info +export type SessionStatusInfo = + | { type: "idle" } + | { type: "retry"; attempt: number; message: string; next: number } + | { type: "busy" } + +// Message types from MessageV2 +export interface MessageInfo { + id: string + sessionID: string + role: "user" | "assistant" + time: { + created: number + completed?: number + } +} + +// Part types - simplified for UI display +export type MessagePart = + | { type: "text"; id: string; text: string } + | { type: "tool"; id: string; tool: string; state: ToolState } + | { type: "reasoning"; id: string; text: string } + +export type ToolState = + | { status: "pending"; input: Record } + | { status: "running"; input: Record; title?: string } + | { status: "completed"; input: Record; output: string; title: string } + | { status: "error"; input: Record; error: string } + +// Permission request from PermissionNext.Request +export interface PermissionRequest { + id: string + sessionID: string + permission: string + patterns: string[] + metadata: Record + always: string[] + tool?: { + messageID: string + callID: string + } +} + +// SSE Event types - based on BusEvent definitions +export type SSEEvent = + | { type: "server.connected"; properties: Record } + | { type: "server.heartbeat"; properties: Record } + | { type: "session.created"; properties: { info: SessionInfo } } + | { type: "session.updated"; properties: { info: SessionInfo } } + | { type: "session.status"; properties: { sessionID: string; status: SessionStatusInfo } } + | { type: "session.idle"; properties: { sessionID: string } } + | { type: "message.updated"; properties: { info: MessageInfo } } + | { type: "message.part.updated"; properties: { part: MessagePart; delta?: string } } + | { type: "permission.asked"; properties: PermissionRequest } + | { + type: "permission.replied" + properties: { sessionID: string; requestID: string; reply: "once" | "always" | "reject" } + } + | { type: "todo.updated"; properties: { sessionID: string; items: TodoItem[] } } + +export interface TodoItem { + id: string + content: string + status: "pending" | "in_progress" | "completed" +} + +// Server connection config +export interface ServerConfig { + baseUrl: string + password: string +} diff --git a/packages/kilo-vscode/src/types/eventsource.d.ts b/packages/kilo-vscode/src/types/eventsource.d.ts new file mode 100644 index 000000000..2a98b63f0 --- /dev/null +++ b/packages/kilo-vscode/src/types/eventsource.d.ts @@ -0,0 +1,36 @@ +declare module "eventsource" { + interface EventSourceInit { + headers?: Record + https?: Record + proxy?: string + rejectUnauthorized?: boolean + withCredentials?: boolean + } + + interface MessageEvent { + data: string + lastEventId: string + origin: string + } + + class EventSource { + static readonly CONNECTING: 0 + static readonly OPEN: 1 + static readonly CLOSED: 2 + + readonly readyState: 0 | 1 | 2 + readonly url: string + + constructor(url: string, eventSourceInitDict?: EventSourceInit) + + onopen: ((event: Event) => void) | null + onmessage: ((event: MessageEvent) => void) | null + onerror: ((event: Event) => void) | null + + close(): void + addEventListener(type: string, listener: (event: MessageEvent) => void): void + removeEventListener(type: string, listener: (event: MessageEvent) => void): void + } + + export = EventSource +} diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 6b52d87bb..1a74cf48a 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -1,4 +1,5 @@ import { Component, createSignal, onMount, onCleanup, Switch, Match } from "solid-js"; +import Settings, { type ConnectionState } from "./components/Settings"; type ViewType = | "newTask" @@ -12,6 +13,20 @@ interface ActionMessage { action: string; } +interface ReadyMessage { + type: "ready"; + serverInfo?: { + port: number; + }; +} + +interface ConnectionStateMessage { + type: "connectionState"; + state: ConnectionState; +} + +type WebviewMessage = ActionMessage | ReadyMessage | ConnectionStateMessage; + const DummyView: Component<{ title: string }> = (props) => { return (
= (props) => { const App: Component = () => { const [currentView, setCurrentView] = createSignal("newTask"); + const [serverPort, setServerPort] = createSignal(null); + const [connectionState, setConnectionState] = createSignal("disconnected"); const handleMessage = (event: MessageEvent) => { - const message = event.data as ActionMessage; + // Debug: log *all* messages received from the extension host. + console.log("[Kilo New] App: ๐Ÿ“จ window.message received:", { + data: event.data, + origin: (event as any).origin, + }); + + const message = event.data as WebviewMessage; + console.log("[Kilo New] App: ๐Ÿ”Ž Parsed message.type:", (message as any)?.type); - if (message.type === "action") { - switch (message.action) { - case "plusButtonClicked": - setCurrentView("newTask"); - break; - case "marketplaceButtonClicked": - setCurrentView("marketplace"); - break; - case "historyButtonClicked": - setCurrentView("history"); - break; - case "profileButtonClicked": - setCurrentView("profile"); - break; - case "settingsButtonClicked": - setCurrentView("settings"); - break; - } + switch (message.type) { + case "action": + console.log("[Kilo New] App: ๐ŸŽฌ action:", message.action); + switch (message.action) { + case "plusButtonClicked": + setCurrentView("newTask"); + break; + case "marketplaceButtonClicked": + setCurrentView("marketplace"); + break; + case "historyButtonClicked": + setCurrentView("history"); + break; + case "profileButtonClicked": + setCurrentView("profile"); + break; + case "settingsButtonClicked": + setCurrentView("settings"); + break; + } + break; + case "ready": + console.log("[Kilo New] App: โœ… ready:", message.serverInfo); + if (message.serverInfo?.port) { + setServerPort(message.serverInfo.port); + } + break; + case "connectionState": + console.log("[Kilo New] App: ๐Ÿ”„ connectionState:", message.state); + setConnectionState(message.state); + break; + default: + // If the extension sends a new message type, we want to see it immediately. + console.warn("[Kilo New] App: โš ๏ธ Unknown message type:", event.data); + break; } }; onMount(() => { + console.log("[Kilo New] App: ๐Ÿงฉ Mount: adding window.message listener"); window.addEventListener("message", handleMessage); }); onCleanup(() => { + console.log("[Kilo New] App: ๐Ÿงน Cleanup: removing window.message listener"); window.removeEventListener("message", handleMessage); }); @@ -79,7 +122,7 @@ const App: Component = () => { - +
diff --git a/packages/kilo-vscode/webview-ui/src/components/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/Settings.tsx new file mode 100644 index 000000000..771cb066a --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/Settings.tsx @@ -0,0 +1,122 @@ +import { Component } from "solid-js"; + +export type ConnectionState = "connecting" | "connected" | "disconnected"; + +export interface SettingsProps { + port: number | null; + connectionState: ConnectionState; +} + +const Settings: Component = (props) => { + const getStatusColor = () => { + switch (props.connectionState) { + case "connected": + return "var(--vscode-testing-iconPassed, #89d185)"; + case "connecting": + return "var(--vscode-testing-iconQueued, #cca700)"; + case "disconnected": + return "var(--vscode-testing-iconFailed, #f14c4c)"; + } + }; + + const getStatusText = () => { + switch (props.connectionState) { + case "connected": + return "Connected"; + case "connecting": + return "Connecting..."; + case "disconnected": + return "Disconnected"; + } + }; + + return ( +
+

+ Settings +

+ +
+

+ CLI Server +

+ + {/* Connection Status */} +
+ + Status: + +
+ + + {getStatusText()} + +
+
+ + {/* Port Number */} +
+ + Port: + + + {props.port !== null ? props.port : "โ€”"} + +
+
+
+ ); +}; + +export default Settings; diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e1d31b9e7..9adf59d73 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -75,13 +75,15 @@ "@gitlab/gitlab-ai-provider": "3.4.0", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", + "@kilocode/kilo-gateway": "workspace:*", + "@kilocode/kilo-telemetry": "workspace:*", + "@kilocode/plugin": "workspace:*", + "@kilocode/sdk": "workspace:*", "@modelcontextprotocol/sdk": "1.25.2", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", - "@kilocode/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", - "@kilocode/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", "@opentui/core": "0.1.77", @@ -121,8 +123,6 @@ "xdg-basedir": "5.1.0", "yargs": "18.0.0", "zod": "catalog:", - "zod-to-json-schema": "3.24.5", - "@kilocode/kilo-gateway": "workspace:*", - "@kilocode/kilo-telemetry": "workspace:*" + "zod-to-json-schema": "3.24.5" } }