diff --git a/.github/workflows/docs-check-links.yml b/.github/workflows/docs-check-links.yml index 4f5eb581cb6..f83c7960f10 100644 --- a/.github/workflows/docs-check-links.yml +++ b/.github/workflows/docs-check-links.yml @@ -4,6 +4,9 @@ on: push: branches: [main] pull_request: + paths: + - "packages/kilo-docs/**" + - ".github/workflows/docs-check-links.yml" schedule: # Run daily at 9am UTC - cron: "0 9 * * *" diff --git a/README.md b/README.md index 97a3a4c2832..d8d8fff10bb 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ - 🎁 API keys optional - 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3.1 Pro, Claude 4.6 Sonnet & Opus, and GPT-5.2 -

- -

- ## Quick Links - [VS Code Marketplace](https://kilo.ai/vscode-marketplace?utm_source=Readme) (download) diff --git a/bun.lock b/bun.lock index b26f2e9e39d..6bccf5a5e19 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-ui": "workspace:*", @@ -77,7 +77,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -110,7 +110,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -139,7 +139,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@clack/prompts": "1.0.0-alpha.1", "@kilocode/plugin": "workspace:*", @@ -171,7 +171,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.0.33", + "version": "7.0.35", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -181,7 +181,7 @@ }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "@opentelemetry/api": "1.9.0", @@ -201,9 +201,10 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@kobalte/core": "0.13.11", + "lucide-solid": "0.576.0", }, "devDependencies": { "@playwright/test": "1.58.2", @@ -225,7 +226,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-i18n": "workspace:*", @@ -269,7 +270,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.0.33", + "version": "7.0.35", "bin": { "opencode": "./bin/opencode", }, @@ -386,7 +387,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@kilocode/sdk": "workspace:*", "zod": "catalog:", @@ -400,14 +401,14 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.0.33", + "version": "7.0.35", "devDependencies": { "@types/bun": "catalog:", }, }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.0.33", + "version": "7.0.35", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -418,7 +419,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -460,7 +461,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "7.0.33", + "version": "7.0.35", "dependencies": { "zod": "catalog:", }, @@ -2866,6 +2867,8 @@ "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "lucide-solid": ["lucide-solid@0.576.0", "", { "peerDependencies": { "solid-js": "^1.4.7" } }, "sha512-WX1JQJXCE8k4YWfj6pLx57i6AyzmH1LRyGP6LUDzIPgPIki9Pk4sIhSfTfF+jCBj8qRcp5aIz+tUURLJrzGAFw=="], + "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], diff --git a/kilo.gif b/kilo.gif deleted file mode 100644 index 800d05f52a2..00000000000 Binary files a/kilo.gif and /dev/null differ diff --git a/nix/hashes.json b/nix/hashes.json index 9138c40ef00..847be80aa1a 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-nK6w/+ZFZ1e3IF3JQaCdE7mn7FtEuY5fUAJNOiPjTVM=", - "aarch64-linux": "sha256-oOiCU3mNgccNrU3VQydTozb5PhRAmUnoyetDkePakq0=", - "aarch64-darwin": "sha256-qT4DC6mGD2pkNjSX2RJ6+FCRIlDnXEYiHAxurULkgXI=", - "x86_64-darwin": "sha256-4Lew/qSnYVbaJlfP+9fIi2mTuw4mMLYNkFbPNQkJu9k=" + "x86_64-linux": "sha256-NrbWpaXEMxKRZVMwSfn0Zt/iJ+/rAgdkeu8saQtPJWg=", + "aarch64-linux": "sha256-qwV7BBySUF2NpgFpPrYUAyEcOKOEHu0Su9HiDIoGlYk=", + "aarch64-darwin": "sha256-eHGuncX+swJvE3UNKCl6yy9AeWHMkpZK209oEsCOjnc=", + "x86_64-darwin": "sha256-jMqI1kJ/QVzPaOO8cVmm5dh5MFYSJ7YMkyAifgBO9KA=" } } diff --git a/package.json b/package.json index bb30d1c87dc..847f21d8412 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,6 @@ "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch" }, - "version": "7.0.33", + "version": "7.0.35", "peerDependencies": {} } diff --git a/packages/app/package.json b/packages/app/package.json index 0decec0990c..b01c5a9c32f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "7.0.33", + "version": "7.0.35", "description": "", "type": "module", "exports": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f06e8ce23f8..03f5bcb2349 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 8ea1c5bca00..5cc2ce061c0 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.0.33" +version = "7.0.35" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.33/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.35/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.33/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.35/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.33/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.35/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.33/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.35/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.33/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.35/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index 9aef9301c5b..d82b560f8d7 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -4,6 +4,8 @@ remap = ["kilo-docs/docs kilo-docs/pages"] format = "detailed" max_redirects = 2 +max_retries = 3 +retry_wait_time = 5 exclude_path = ["node_modules"] accept = [ @@ -19,7 +21,7 @@ exclude = [ 'https://synthetic.new/user-settings/api', 'https://platform.slack-edge.com/img/add_to_slack.png', 'https://console.groq.com/pages/models', - 'https://glama.ai/settings/gateway/api-keys', + '^https?://([A-Za-z0-9-]+\.)?glama\.ai(/|$)', '^https?://(api|console)\.mistral\.ai', '^https?://(app|console)\.requesty\.ai', '^https?://(cloud|console)\.google\.ai', diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 49b452481ce..a56411dead8 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.0.33", + "version": "7.0.35", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-docs/public/img/kilogif.gif b/packages/kilo-docs/public/img/kilogif.gif deleted file mode 100644 index 800d05f52a2..00000000000 Binary files a/packages/kilo-docs/public/img/kilogif.gif and /dev/null differ diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index f8a9bc5fd76..ee69c932ec9 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-gateway", - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 9ed52fbd82a..7531c4cee7d 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-i18n", - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index dc2ea2fa04b..cddca49c9d5 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-telemetry", - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "description": "Telemetry for Kilo CLI - PostHog analytics integration", diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 989da989136..11bbdb172ac 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "exports": { @@ -73,6 +73,7 @@ "./theme": "./src/theme/index.ts", "./theme/*": "./src/theme/*.ts", "./theme/context": "./src/theme/context.tsx", + "./lucide": "./src/lucide.ts", "./icons/provider": "./src/components/provider-icons/types.ts", "./icons/file-type": "./src/components/file-icons/types.ts", "./icons/app": "./src/components/app-icons/types.ts", @@ -103,6 +104,7 @@ "test:visual:update": "playwright test --update-snapshots" }, "dependencies": { - "@kobalte/core": "0.13.11" + "@kobalte/core": "0.13.11", + "lucide-solid": "0.576.0" } } diff --git a/packages/kilo-ui/src/lucide.ts b/packages/kilo-ui/src/lucide.ts new file mode 100644 index 00000000000..e715ec841d7 --- /dev/null +++ b/packages/kilo-ui/src/lucide.ts @@ -0,0 +1,6 @@ +/** + * Re-export Lucide icons for consumers of @kilocode/kilo-ui. + * Only add icons here that are actually used — esbuild/Vite will + * tree-shake unused exports but explicit re-exports keep the API small. + */ +export { WandSparkles } from "lucide-solid" diff --git a/packages/kilo-ui/tests/visual-regression.spec.ts b/packages/kilo-ui/tests/visual-regression.spec.ts index 42ef6eefa8e..553ce269027 100644 --- a/packages/kilo-ui/tests/visual-regression.spec.ts +++ b/packages/kilo-ui/tests/visual-regression.spec.ts @@ -1,4 +1,14 @@ import { test, expect, type Page } from "@playwright/test" +import { platform } from "node:os" + +const IS_DARWIN = platform() === "darwin" + +// Screenshot baselines are captured on Linux CI — font rendering and anti-aliasing +// differ on macOS, which causes false-positive diffs. Skip the entire suite there. +if (IS_DARWIN) { + console.warn("Visual regression tests must be run on CI, skipping on local macOS.") + test.skip() +} type Story = { id: string @@ -15,9 +25,7 @@ const STORYBOOK_URL = "http://localhost:6006" // Fetched once per worker process — cheap HTTP call to the already-running Storybook async function fetchStories(): Promise { - const res = await fetch(`${STORYBOOK_URL}/index.json`).catch(() => - fetch(`${STORYBOOK_URL}/stories.json`), - ) + const res = await fetch(`${STORYBOOK_URL}/index.json`).catch(() => fetch(`${STORYBOOK_URL}/stories.json`)) if (!res.ok) throw new Error(`Storybook index fetch failed: ${res.status} ${res.statusText}`) const data = (await res.json()) as StoriesIndex const map = data.entries ?? data.stories ?? {} @@ -53,14 +61,14 @@ const SKIP = new Set([ // Generate one test() per story so Playwright's scheduler can distribute // them freely across workers — no manual sharding needed. -const stories = (await fetchStories()).filter((s) => !SKIP.has(s.id)) +// Skip fetching stories on macOS since test.skip() above already marks the file skipped. +const stories = IS_DARWIN ? [] : (await fetchStories()).filter((s) => !SKIP.has(s.id)) for (const story of stories) { test(`${story.title} / ${story.name}`, async ({ page }) => { - await page.goto( - `/iframe.html?id=${story.id}&viewMode=story&globals=colorScheme:dark;theme:kilo`, - { waitUntil: "load" }, - ) + await page.goto(`/iframe.html?id=${story.id}&viewMode=story&globals=colorScheme:dark;theme:kilo`, { + waitUntil: "load", + }) await disableAnimations(page) // Wait for Kobalte/SolidJS to finish hydrating interactive components await page.waitForSelector("#storybook-root *", { state: "attached" }) diff --git a/packages/kilo-vscode/README.md b/packages/kilo-vscode/README.md index fa059df7cb4..a4ff8d16b5d 100644 --- a/packages/kilo-vscode/README.md +++ b/packages/kilo-vscode/README.md @@ -20,10 +20,6 @@ - 🎁 API keys optional - 💡 **Get $20 in bonus credits when you top-up for the first time** Credits can be used with 500+ models like Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5 -

- -

- ## Quick Links - [VS Code Marketplace](https://kilo.ai/vscode-marketplace?utm_source=Readme) (download) diff --git a/packages/kilo-vscode/bunfig.toml b/packages/kilo-vscode/bunfig.toml new file mode 100644 index 00000000000..2c6410f31e1 --- /dev/null +++ b/packages/kilo-vscode/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./tests/setup/vscode-mock.ts"] diff --git a/packages/kilo-vscode/docs/features/prompt-improvement.md b/packages/kilo-vscode/docs/features/prompt-improvement.md deleted file mode 100644 index 4e3c3b1a70e..00000000000 --- a/packages/kilo-vscode/docs/features/prompt-improvement.md +++ /dev/null @@ -1,23 +0,0 @@ -# Prompt Improvement Feature - -**Priority:** P3 -**Status:** ❌ Not started -**Issue:** [Kilo-Org/kilo#594](https://github.com/Kilo-Org/kilo/issues/594) (private) - -## Summary - -A feature to help users improve their prompts before sending — likely an "Improve prompt" button in the chat input that rewrites or enhances the user's message using AI assistance. - -## Remaining Work - -Details are tracked in the private issue. At a high level: - -- Add an "Improve prompt" button or keyboard shortcut in the chat input area -- When triggered, send the current draft message to the CLI/API to be rewritten into a more effective prompt -- Show the improved version as a preview; let the user accept, edit, or dismiss it -- The improvement request should go through the existing session or a lightweight one-shot endpoint - -## Implementation Notes - -- This feature requires CLI-side support for a "rewrite prompt" endpoint, or it can be done client-side by sending a meta-prompt to the model -- Design should avoid blocking the main chat input while the improvement request is in flight diff --git a/packages/kilo-vscode/docs/opencode-migration-plan.md b/packages/kilo-vscode/docs/opencode-migration-plan.md index 089b891b0b1..a14df516ff3 100644 --- a/packages/kilo-vscode/docs/opencode-migration-plan.md +++ b/packages/kilo-vscode/docs/opencode-migration-plan.md @@ -78,7 +78,6 @@ Open issues from the [GitHub project board](https://github.com/orgs/Kilo-Org/pro | [Session Preview Improvements](features/session-preview-improvements.md) | ❌ Not started | Evaluate showing first message snippet or improving title generation | P2 | | [Subagent Visibility](features/subagent-visibility.md) | ❌ Not started | Show inline indicator of what a subagent is doing in single-session sidebar view | P2 | | [Terminal Command Output Visibility](features/terminal-output-visibility.md) | ❌ Not started | Show actual command, truncated output, and success/failure in chat | P1 | -| [Prompt Improvement Feature](features/prompt-improvement.md) | ❌ Not started | Button/shortcut to rewrite the current draft message into a better prompt | P3 | ### Migration (old extension → new) diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index a31aad7d90c..1c5461f59f8 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -2,7 +2,7 @@ "name": "kilo-code", "displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete", "description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.", - "version": "7.0.33", + "version": "7.0.35", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 68e78813db9..a876282e4ea 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1,11 +1,16 @@ import * as path from "path" import * as vscode from "vscode" import { z } from "zod" -import type { KiloClient, Session, SessionStatus, Event, TextPartInput, FilePartInput, Config } from "@kilocode/sdk/v2/client" -import { - type KiloConnectionService, - type KilocodeNotification, -} from "./services/cli-backend" +import type { + KiloClient, + Session, + SessionStatus, + Event, + TextPartInput, + FilePartInput, + Config, +} from "@kilocode/sdk/v2/client" +import { type KiloConnectionService, type KilocodeNotification } from "./services/cli-backend" import type { EditorContext, CloudSessionData } from "./services/cli-backend/types" import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController" import { handleChatCompletionRequest } from "./services/autocomplete/chat-autocomplete/handleChatCompletionRequest" @@ -507,6 +512,29 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "variantsLoaded", variants }) break } + case "enhancePrompt": { + const sdkClient = this.client + if (!sdkClient) { + this.postMessage({ type: "enhancePromptError", error: "Not connected to CLI backend", requestId: message.requestId }) + break + } + void sdkClient.enhancePrompt + .enhance({ text: message.text }, { throwOnError: true }) + .then(({ data }) => { + this.postMessage({ type: "enhancePromptResult", text: data.text, requestId: message.requestId }) + }) + .catch((err: unknown) => { + const msg = getErrorMessage(err) || "Failed to enhance prompt" + console.error("[Kilo New] KiloProvider: Failed to enhance prompt:", err) + vscode.window.showErrorMessage(`Enhance prompt failed: ${msg}`) + this.postMessage({ + type: "enhancePromptError", + error: msg, + requestId: message.requestId, + }) + }) + break + } } }) } @@ -1181,7 +1209,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Step 1: Import the cloud session with fresh IDs let session: Session | undefined try { - const importResult = await this.client.kilo.cloud.session.import({ sessionId: cloudSessionId, directory: workspaceDir }) + const importResult = await this.client.kilo.cloud.session.import({ + sessionId: cloudSessionId, + directory: workspaceDir, + }) session = importResult.data as Session | undefined } catch (error) { console.error("[Kilo New] KiloProvider: ❌ Cloud session import failed:", error) @@ -1292,10 +1323,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } try { - const { data: updated } = await this.client.global.config.update( - { config: partial }, - { throwOnError: true }, - ) + const { data: updated } = await this.client.global.config.update({ config: partial }, { throwOnError: true }) const message = { type: "configUpdated", diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 964cb58ed01..9d2ec2215e9 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -9,7 +9,7 @@ import { buildWebviewHtml } from "../utils" import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager" import { WorktreeStateManager } from "./WorktreeStateManager" import { GitStatsPoller } from "./GitStatsPoller" -import { GitOps } from "./GitOps" +import { GitOps, type ApplyConflict } from "./GitOps" import { versionedName } from "./branch-name" import { normalizePath } from "./git-import" import { SetupScriptService } from "./SetupScriptService" @@ -48,6 +48,9 @@ export class AgentManagerProvider implements vscode.Disposable { private statsPoller: GitStatsPoller private gitOps: GitOps private cachedDiffTarget: { directory: string; baseBranch: string } | undefined + private cachedWorktreeStats: Record | undefined + private cachedLocalStats: Record | undefined + private applyingWorktreeId: string | undefined constructor( private readonly extensionUri: vscode.Uri, @@ -63,10 +66,14 @@ export class AgentManagerProvider implements vscode.Disposable { getWorkspaceRoot: () => this.getWorkspaceRoot(), getClient: () => this.connectionService.getClient(), onStats: (stats) => { - this.postToWebview({ type: "agentManager.worktreeStats", stats }) + const msg = { type: "agentManager.worktreeStats", stats } + this.cachedWorktreeStats = msg + this.postToWebview(msg) }, onLocalStats: (stats) => { - this.postToWebview({ type: "agentManager.localStats", stats }) + const msg = { type: "agentManager.localStats", stats } + this.cachedLocalStats = msg + this.postToWebview(msg) }, log: (...args) => this.log(...args), git: this.gitOps, @@ -220,6 +227,10 @@ export class AgentManagerProvider implements vscode.Disposable { return } this.pushState() + // Re-send cached stats so the webview gets them even if the poller + // already emitted before the webview was ready to receive messages. + if (this.cachedWorktreeStats) this.postToWebview(this.cachedWorktreeStats) + if (this.cachedLocalStats) this.postToWebview(this.cachedLocalStats) // Refresh sessions after pushState so the webview's sessionsLoaded // handler is guaranteed to be registered (requestState fires from // onMount). Without this, the initial refreshSessions() in @@ -285,6 +296,17 @@ export class AgentManagerProvider implements vscode.Disposable { void this.onRequestWorktreeDiff(msg.sessionId) return null } + if (type === "agentManager.applyWorktreeDiff" && typeof msg.worktreeId === "string") { + const selectedFiles = Array.isArray(msg.selectedFiles) + ? [ + ...new Set( + msg.selectedFiles.filter((file): file is string => typeof file === "string").map((file) => file.trim()), + ), + ].filter((file) => file.length > 0) + : undefined + void this.onApplyWorktreeDiff(msg.worktreeId, selectedFiles) + return null + } if (type === "agentManager.startDiffWatch" && typeof msg.sessionId === "string") { this.startDiffPolling(msg.sessionId) return null @@ -1328,24 +1350,99 @@ export class AgentManagerProvider implements vscode.Disposable { // Helpers // --------------------------------------------------------------------------- + private postApplyResult( + worktreeId: string, + status: "checking" | "applying" | "success" | "conflict" | "error", + message: string, + conflicts?: ApplyConflict[], + ): void { + this.postToWebview({ + type: "agentManager.applyWorktreeDiffResult", + worktreeId, + status, + message, + conflicts, + }) + } + + private async onApplyWorktreeDiff(worktreeId: string, selectedFiles?: string[]): Promise { + if (this.applyingWorktreeId) { + this.postApplyResult(worktreeId, "error", "Another apply operation is already in progress") + return + } + + if (selectedFiles && selectedFiles.length === 0) { + this.postApplyResult(worktreeId, "error", "Select at least one file to apply") + return + } + + const state = this.getStateManager() + const root = this.getWorkspaceRoot() + if (!state || !root) { + this.postApplyResult(worktreeId, "error", "Open a git workspace to apply changes") + return + } + + const worktree = state.getWorktree(worktreeId) + if (!worktree) { + this.postApplyResult(worktreeId, "error", "Worktree not found") + return + } + + this.applyingWorktreeId = worktreeId + + try { + this.postApplyResult(worktreeId, "checking", "Checking for conflicts...") + const patch = await this.gitOps.buildWorktreePatch(worktree.path, worktree.parentBranch, selectedFiles) + + if (!patch.trim()) { + this.postApplyResult(worktreeId, "success", "No changes to apply") + return + } + + const check = await this.gitOps.checkApplyPatch(root, patch) + if (!check.ok) { + this.postApplyResult(worktreeId, "conflict", check.message, check.conflicts) + return + } + + this.postApplyResult(worktreeId, "applying", "Applying changes to local branch...") + const applied = await this.gitOps.applyPatch(root, patch) + if (!applied.ok) { + const conflict = applied.conflicts.length > 0 + const status = conflict ? "conflict" : "error" + this.postApplyResult(worktreeId, status, applied.message, applied.conflicts) + return + } + + this.postApplyResult(worktreeId, "success", "Applied worktree changes to local branch") + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + this.log("Failed to apply worktree diff:", message) + this.postApplyResult(worktreeId, "error", message) + } finally { + this.applyingWorktreeId = undefined + } + } + // --------------------------------------------------------------------------- // Diff polling // --------------------------------------------------------------------------- - /** Open a file from a worktree session in the VS Code editor. */ + /** Open a file from a worktree or local session in the VS Code editor. */ private openWorktreeFile(sessionId: string, relativePath: string): void { const state = this.getStateManager() if (!state) return const session = state.getSession(sessionId) - if (!session?.worktreeId) return - const worktree = state.getWorktree(session.worktreeId) - if (!worktree) return + if (!session) return + const base = session.worktreeId ? state.getWorktree(session.worktreeId)?.path : this.getWorkspaceRoot() + if (!base) return // Resolve real paths to prevent symlink traversal and normalize for // consistent comparison on both Unix and Windows. let resolved: string try { - const root = fs.realpathSync(worktree.path) - resolved = fs.realpathSync(path.resolve(worktree.path, relativePath)) + const root = fs.realpathSync(base) + resolved = fs.realpathSync(path.resolve(base, relativePath)) // Directory-boundary check: append path.sep so "/foo/bar" won't match "/foo/bar2/..." if (resolved !== root && !resolved.startsWith(root + path.sep)) return } catch (err) { @@ -1386,18 +1483,27 @@ export class AgentManagerProvider implements vscode.Disposable { return { directory: worktree.path, baseBranch: worktree.parentBranch } } - /** Resolve diff target for the local workspace — diffs against the remote tracking branch. */ + /** Resolve diff target for the local workspace — diffs against the remote tracking + * branch, falling back to the repo's default branch, and ultimately to HEAD so + * local-only repos (no remote) still show working-tree changes in the diff panel. */ private async resolveLocalDiffTarget(): Promise<{ directory: string; baseBranch: string } | undefined> { const root = this.getWorkspaceRoot() - if (!root) return undefined - const branch = await this.gitOps.currentBranch(root) - if (!branch || branch === "HEAD") return undefined - const tracking = await this.gitOps.resolveTrackingBranch(root, branch) - if (!tracking) { - this.log("Local diff: no remote tracking branch found") + if (!root) { + this.log("Local diff: no workspace root") return undefined } - return { directory: root, baseBranch: tracking } + const branch = await this.gitOps.currentBranch(root) + if (!branch || branch === "HEAD") { + this.log("Local diff: detached HEAD or no branch") + return undefined + } + const tracking = await this.gitOps.resolveTrackingBranch(root, branch) + const defaultBranch = tracking ? undefined : await this.gitOps.resolveDefaultBranch(root, branch) + const base = tracking ?? defaultBranch ?? "HEAD" + this.log( + `Local diff: branch=${branch} tracking=${tracking ?? "none"} default=${defaultBranch ?? "none"} base=${base}`, + ) + return { directory: root, baseBranch: base } } /** One-shot diff fetch with loading indicators. Resolves target async, then fetches. */ @@ -1420,14 +1526,16 @@ export class AgentManagerProvider implements vscode.Disposable { this.postToWebview({ type: "agentManager.worktreeDiffLoading", sessionId, loading: true }) try { const client = this.connectionService.getClient() - this.log(`Fetching worktree diff for session ${sessionId}: dir=${target.directory}, base=${target.baseBranch}`) const { data: diffs } = await client.worktree.diff( - { directory: target.directory }, + { directory: target.directory, base: target.baseBranch }, { throwOnError: true }, ) + this.log(`Worktree diff returned ${diffs.length} file(s) for session ${sessionId}`) - const hash = diffs.map((d: FileDiff) => `${d.file}:${d.status}:${d.additions}:${d.deletions}:${d.after.length}`).join("|") + const hash = diffs + .map((d: FileDiff) => `${d.file}:${d.status}:${d.additions}:${d.deletions}:${d.after.length}`) + .join("|") this.lastDiffHash = hash this.diffSessionId = sessionId @@ -1447,11 +1555,13 @@ export class AgentManagerProvider implements vscode.Disposable { try { const client = this.connectionService.getClient() const { data: diffs } = await client.worktree.diff( - { directory: target.directory }, + { directory: target.directory, base: target.baseBranch }, { throwOnError: true }, ) - const hash = diffs.map((d: FileDiff) => `${d.file}:${d.status}:${d.additions}:${d.deletions}:${d.after.length}`).join("|") + const hash = diffs + .map((d: FileDiff) => `${d.file}:${d.status}:${d.additions}:${d.deletions}:${d.after.length}`) + .join("|") if (hash === this.lastDiffHash && this.diffSessionId === sessionId) return this.lastDiffHash = hash this.diffSessionId = sessionId diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts index 1dd7c3bc6cc..6fa3a2e624f 100644 --- a/packages/kilo-vscode/src/agent-manager/GitOps.ts +++ b/packages/kilo-vscode/src/agent-manager/GitOps.ts @@ -1,4 +1,7 @@ import * as nodePath from "path" +import * as os from "os" +import * as cp from "child_process" +import * as fs from "fs/promises" import simpleGit from "simple-git" export interface GitOpsOptions { @@ -8,6 +11,34 @@ export interface GitOpsOptions { runGit?: (args: string[], cwd: string) => Promise } +export interface ApplyConflict { + file?: string + reason: string +} + +export interface ApplyCheckResult { + ok: boolean + conflicts: ApplyConflict[] + message: string +} + +export interface ApplyPatchResult { + ok: boolean + conflicts: ApplyConflict[] + message: string +} + +interface ExecOptions { + env?: NodeJS.ProcessEnv + stdin?: string +} + +interface ExecResult { + code: number + stdout: string + stderr: string +} + export class GitOps { private lastFetch = new Map() private inflightFetch = new Map>() @@ -107,25 +138,245 @@ export class GitOps { return job } - async countMissingOriginCommits(cwd: string, parentBranch: string): Promise { + /** + * Compute working-tree stats (staged + unstaged + untracked) without requiring + * a remote or base branch — mirrors the superset approach of running + * `git diff --numstat` and `git ls-files --others`. + */ + async workingTreeStats(cwd: string): Promise<{ files: number; additions: number; deletions: number }> { + // Staged + unstaged changes relative to HEAD (like superset's dual + // git diff --cached --numstat + git diff --numstat, combined). + const [numstat, untracked] = await Promise.all([ + this.raw(["diff", "HEAD", "--numstat"], cwd).catch(() => ""), + this.raw(["ls-files", "--others", "--exclude-standard"], cwd).catch(() => ""), + ]) + + const tracked = numstat + ? numstat.split("\n").reduce( + (acc, line) => { + if (!line.trim()) return acc + const parts = line.split("\t") + return { + files: acc.files + 1, + additions: acc.additions + (parts[0] !== "-" ? parseInt(parts[0], 10) || 0 : 0), + deletions: acc.deletions + (parts[1] !== "-" ? parseInt(parts[1], 10) || 0 : 0), + } + }, + { files: 0, additions: 0, deletions: 0 }, + ) + : { files: 0, additions: 0, deletions: 0 } + + // Count lines in untracked files as additions (like superset's + // applyUntrackedLineCount). Cap at 1MB to avoid reading huge binaries. + if (!untracked) return tracked + + const paths = untracked.split("\n").filter((line) => line.trim()) + const counts = await Promise.all( + paths.map(async (p) => { + try { + const full = nodePath.resolve(cwd, p) + const stat = await fs.stat(full) + if (stat.size > 1_000_000) return 0 + const content = await fs.readFile(full, "utf-8") + return content.split("\n").length + } catch (err) { + this.log(`Failed to read untracked file ${p}:`, err) + return 0 + } + }), + ) + + return { + files: tracked.files + paths.length, + additions: tracked.additions + counts.reduce((sum, count) => sum + count, 0), + deletions: tracked.deletions, + } + } + + /** + * Count commits ahead and behind in a single `rev-list --left-right --count` + * call (like superset's approach). Falls back through upstream → remote/branch + * → remote/parentBranch → parentBranch. + */ + async aheadBehind(cwd: string, parentBranch: string): Promise<{ ahead: number; behind: number }> { const upstream = await this.raw(["rev-parse", "--abbrev-ref", "@{upstream}"], cwd).catch(() => "") const branch = await this.raw(["branch", "--show-current"], cwd).catch(() => "") const remote = await this.resolveRemote(cwd, branch) await this.refreshRemote(cwd, remote) - if (upstream) { - const count = await this.raw(["rev-list", "--count", `${upstream}..HEAD`], cwd).catch(() => "0") - return parseInt(count, 10) || 0 + const ref = (() => { + if (upstream) return upstream + const remoteBranch = branch ? `${remote}/${branch}` : "" + // hasRemoteRef is async, so we can't use it inline — resolve below + return { remoteBranch, remoteParent: `${remote}/${parentBranch}`, parentBranch } + })() + + if (typeof ref === "string") { + return this.parseLeftRight(cwd, ref) } - const remoteBranch = branch ? `${remote}/${branch}` : "" - const hasRemoteBranch = remoteBranch ? await this.hasRemoteRef(cwd, remoteBranch) : false + if (ref.remoteBranch && (await this.hasRemoteRef(cwd, ref.remoteBranch))) { + return this.parseLeftRight(cwd, ref.remoteBranch) + } + if (await this.hasRemoteRef(cwd, ref.remoteParent)) { + return this.parseLeftRight(cwd, ref.remoteParent) + } + return this.parseLeftRight(cwd, ref.parentBranch) + } - const remoteParent = `${remote}/${parentBranch}` - const hasRemoteParent = await this.hasRemoteRef(cwd, remoteParent) + private async parseLeftRight(cwd: string, ref: string): Promise<{ ahead: number; behind: number }> { + const out = await this.raw(["rev-list", "--left-right", "--count", `${ref}...HEAD`], cwd).catch(() => "0\t0") + const [behind, ahead] = out.split(/\s+/).map((s) => parseInt(s, 10) || 0) + return { ahead, behind } + } - const ref = hasRemoteBranch ? remoteBranch : hasRemoteParent ? remoteParent : parentBranch - const count = await this.raw(["rev-list", "--count", `${ref}..HEAD`], cwd).catch(() => "0") - return parseInt(count, 10) || 0 + async buildWorktreePatch(sourcePath: string, baseBranch: string, selectedFiles?: string[]): Promise { + const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), "kilo-apply-")) + const index = nodePath.join(tmp, "index") + const env = { ...process.env, GIT_INDEX_FILE: index } + const files = (selectedFiles ?? []) + .map((file) => file.trim()) + .filter((file) => file.length > 0 && !nodePath.isAbsolute(file) && !file.split(/[\\/]/).includes("..")) + const pathspec = files.length > 0 ? files : ["."] + + try { + const base = (await this.raw(["merge-base", "HEAD", baseBranch], sourcePath)).trim() + const baseTree = (await this.raw(["rev-parse", `${base}^{tree}`], sourcePath)).trim() + + const read = await this.exec(["read-tree", "HEAD"], sourcePath, { env }) + if (read.code !== 0) { + throw new Error(read.stderr.trim() || "Failed to initialize temporary index") + } + + const add = await this.exec(["add", "-A", "--", ...pathspec], sourcePath, { env }) + if (add.code !== 0) { + throw new Error(add.stderr.trim() || "Failed to stage worktree snapshot") + } + + const treeResult = await this.exec(["write-tree"], sourcePath, { env }) + if (treeResult.code !== 0) { + throw new Error(treeResult.stderr.trim() || "Failed to snapshot worktree index") + } + + const tree = treeResult.stdout.trim() + const diff = await this.exec( + ["diff", "--binary", "--full-index", "--find-renames", "--no-color", baseTree, tree], + sourcePath, + ) + if (diff.code !== 0) { + throw new Error(diff.stderr.trim() || "Failed to generate patch") + } + + return diff.stdout + } finally { + await fs.rm(tmp, { recursive: true, force: true }) + } + } + + async checkApplyPatch(targetPath: string, patch: string): Promise { + if (!patch.trim()) { + return { ok: true, conflicts: [], message: "No changes to apply" } + } + + const result = await this.exec(["apply", "--3way", "--check", "--whitespace=nowarn", "-"], targetPath, { + stdin: patch, + }) + if (result.code === 0) { + return { ok: true, conflicts: [], message: "Patch applies cleanly" } + } + + const output = [result.stderr, result.stdout].filter(Boolean).join("\n") + const message = output.trim() || "Patch does not apply cleanly" + const conflicts = this.parseApplyConflicts(output) + return { ok: false, conflicts, message } + } + + async applyPatch(targetPath: string, patch: string): Promise { + if (!patch.trim()) { + return { ok: true, conflicts: [], message: "No changes to apply" } + } + + const result = await this.exec(["apply", "--3way", "--whitespace=nowarn", "-"], targetPath, { stdin: patch }) + if (result.code === 0) { + return { ok: true, conflicts: [], message: "Patch applied" } + } + + const output = [result.stderr, result.stdout].filter(Boolean).join("\n") + const message = output.trim() || "Failed to apply patch" + const conflicts = this.parseApplyConflicts(output) + return { ok: false, conflicts, message } + } + + private parseApplyConflicts(output: string): ApplyConflict[] { + const lines = output + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter(Boolean) + + const seen = new Set() + const conflicts: ApplyConflict[] = [] + + for (const line of lines) { + const patchFailed = /^error:\s+patch failed:\s+(.+?):\d+$/i.exec(line) + if (patchFailed) { + const file = patchFailed[1]! + const reason = "patch failed" + const key = `${file}:${reason}` + if (seen.has(key)) continue + seen.add(key) + conflicts.push({ file, reason }) + continue + } + + const fileReason = + /^error:\s+(.+?):\s+(does not match index|patch does not apply|cannot read the current contents.*)$/i.exec(line) + if (fileReason) { + const file = fileReason[1]! + const reason = fileReason[2]! + const key = `${file}:${reason}` + if (seen.has(key)) continue + seen.add(key) + conflicts.push({ file, reason }) + continue + } + } + + if (conflicts.length > 0) return conflicts + const first = lines[0] + if (first) return [{ reason: first }] + return [{ reason: "Patch does not apply cleanly" }] + } + + private exec(args: string[], cwd: string, options?: ExecOptions): Promise { + return new Promise((resolve) => { + const child = cp.execFile( + "git", + args, + { + cwd, + env: options?.env, + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + }, + (error, stdout, stderr) => { + if (!error) { + resolve({ code: 0, stdout, stderr }) + return + } + const exec = error as cp.ExecException + const code = typeof exec.code === "number" ? exec.code : 1 + const fallback = exec.message || "Git command failed" + resolve({ code, stdout: stdout ?? "", stderr: stderr || fallback }) + }, + ) + + if (options?.stdin !== undefined) { + if (!child.stdin) { + resolve({ code: 1, stdout: "", stderr: "stdin not available for git process" }) + return + } + child.stdin.end(options.stdin) + } + }) } } diff --git a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts index 714a48e7b42..b92a2474be4 100644 --- a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts @@ -4,16 +4,20 @@ import type { GitOps } from "./GitOps" export interface WorktreeStats { worktreeId: string + files: number additions: number deletions: number - commits: number + ahead: number + behind: number } export interface LocalStats { branch: string + files: number additions: number deletions: number - commits: number + ahead: number + behind: number } interface GitStatsPollerOptions { @@ -34,7 +38,10 @@ export class GitStatsPoller { private lastHash: string | undefined private lastLocalHash: string | undefined private lastLocalStats: LocalStats | undefined - private lastStats: Record = {} + private lastStats: Record< + string, + { files: number; additions: number; deletions: number; ahead: number; behind: number } + > = {} private readonly intervalMs: number private readonly git: GitOps @@ -110,20 +117,25 @@ export class GitStatsPoller { await Promise.all( worktrees.map(async (wt) => { try { - const { data: diffs } = await client.worktree.diff({ directory: wt.path }, { throwOnError: true }) + const [{ data: diffs }, ab] = await Promise.all([ + client.worktree.diff({ directory: wt.path, base: wt.parentBranch }, { throwOnError: true }), + this.git.aheadBehind(wt.path, wt.parentBranch), + ]) + const files = diffs.length const additions = diffs.reduce((sum: number, diff: FileDiff) => sum + diff.additions, 0) const deletions = diffs.reduce((sum: number, diff: FileDiff) => sum + diff.deletions, 0) - const commits = await this.git.countMissingOriginCommits(wt.path, wt.parentBranch) - return { worktreeId: wt.id, additions, deletions, commits } + return { worktreeId: wt.id, files, additions, deletions, ahead: ab.ahead, behind: ab.behind } } catch (err) { this.options.log(`Failed to fetch worktree stats for ${wt.branch} (${wt.path}):`, err) const prev = this.lastStats[wt.id] if (!prev) return undefined return { worktreeId: wt.id, + files: prev.files, additions: prev.additions, deletions: prev.deletions, - commits: prev.commits, + ahead: prev.ahead, + behind: prev.behind, } } }), @@ -132,19 +144,25 @@ export class GitStatsPoller { if (stats.length === 0) return - const hash = stats.map((item) => `${item.worktreeId}:${item.additions}:${item.deletions}:${item.commits}`).join("|") + const hash = stats + .map( + (item) => `${item.worktreeId}:${item.files}:${item.additions}:${item.deletions}:${item.ahead}:${item.behind}`, + ) + .join("|") if (hash === this.lastHash) return this.lastHash = hash this.lastStats = stats.reduce( (acc, item) => { acc[item.worktreeId] = { + files: item.files, additions: item.additions, deletions: item.deletions, - commits: item.commits, + ahead: item.ahead, + behind: item.behind, } return acc }, - {} as Record, + {} as Record, ) this.options.onStats(stats) @@ -160,37 +178,33 @@ export class GitStatsPoller { const tracking = await this.git.resolveTrackingBranch(root, branch) - // When the client is unavailable, preserve last-known stats rather - // than emitting zeros (which would falsely indicate a clean state). - if (!client) { - if (this.lastLocalStats && this.lastLocalStats.branch === branch) return - const stats: LocalStats = { branch, additions: 0, deletions: 0, commits: 0 } - const hash = `local:${branch}:0:0:0` - if (hash === this.lastLocalHash) return - this.lastLocalHash = hash - this.lastLocalStats = stats - this.options.onLocalStats(stats) - return - } - - // When no tracking branch exists (e.g. new local branch with no upstream - // and no origin/ ref), compute diff+commit stats against the - // repo's default branch so the UI still shows meaningful numbers. const base = tracking ?? (await this.git.resolveDefaultBranch(root, branch)) + let files: number let additions: number let deletions: number - let commits: number + let ahead: number + let behind: number try { - if (base) { - const { data: diffs } = await client.worktree.diff({ directory: root }, { throwOnError: true }) + if (base && client) { + this.options.log(`Local stats: using HTTP client with base=${base}`) + const [{ data: diffs }, ab] = await Promise.all([ + client.worktree.diff({ directory: root, base }, { throwOnError: true }), + this.git.aheadBehind(root, base), + ]) + files = diffs.length additions = diffs.reduce((sum: number, d: FileDiff) => sum + d.additions, 0) deletions = diffs.reduce((sum: number, d: FileDiff) => sum + d.deletions, 0) - commits = await this.git.countMissingOriginCommits(root, base) + ahead = ab.ahead + behind = ab.behind } else { - additions = 0 - deletions = 0 - commits = 0 + this.options.log(`Local stats: fallback to workingTreeStats (base=${base ?? "none"} client=${!!client})`) + const wt = await this.git.workingTreeStats(root) + files = wt.files + additions = wt.additions + deletions = wt.deletions + ahead = 0 + behind = 0 } } catch (err) { this.options.log("Failed to fetch local diff stats:", err) @@ -198,11 +212,15 @@ export class GitStatsPoller { return } - const hash = `local:${branch}:${additions}:${deletions}:${commits}` - if (hash === this.lastLocalHash) return + const hash = `local:${branch}:${files}:${additions}:${deletions}:${ahead}:${behind}` + if (hash === this.lastLocalHash) { + this.options.log(`Local stats: unchanged (${hash})`) + return + } this.lastLocalHash = hash - const stats: LocalStats = { branch, additions, deletions, commits } + this.options.log(`Local stats: emitting files=${files} +${additions} -${deletions} ↑${ahead} ↓${behind}`) + const stats: LocalStats = { branch, files, additions, deletions, ahead, behind } this.lastLocalStats = stats this.options.onLocalStats(stats) } catch (err) { diff --git a/packages/kilo-vscode/tests/setup/vscode-mock.ts b/packages/kilo-vscode/tests/setup/vscode-mock.ts new file mode 100644 index 00000000000..3e73903ffeb --- /dev/null +++ b/packages/kilo-vscode/tests/setup/vscode-mock.ts @@ -0,0 +1,104 @@ +/** + * Shared vscode module mock for all Bun unit tests. + * + * Registered as a preload in bunfig.toml so the mock is applied ONCE before + * any test file loads. This eliminates race conditions when multiple test + * files each call mock.module("vscode", ...) with competing mock shapes. + */ +import { mock } from "bun:test" + +const kind = (value: string): { value: string; append: (part: string) => ReturnType } => ({ + value, + append: (part: string) => kind(`${value}.${part}`), +}) + +const noop = () => {} + +const mockUri = { + parse: (value: string) => ({ scheme: "https", authority: "", path: value, query: "", fragment: "", fsPath: value }), + file: (path: string) => ({ scheme: "file", authority: "", path, query: "", fragment: "", fsPath: path }), + joinPath: (base: { fsPath: string }, ...segments: string[]) => { + const joined = [base.fsPath, ...segments].join("/") + return { scheme: "file", authority: "", path: joined, query: "", fragment: "", fsPath: joined } + }, +} + +const mockVscode = { + Uri: mockUri, + extensions: { + getExtension: (id: string) => { + if (id === "vscode.git") return undefined + return { packageJSON: { version: "test", contributes: { configuration: { properties: {} } } }, isActive: true, exports: undefined } + }, + }, + env: { + appName: "VS Code", + language: "en", + machineId: "test-machine", + isTelemetryEnabled: false, + shell: "/bin/bash", + openExternal: noop, + }, + version: "1.90.0", + workspace: { + workspaceFolders: [{ uri: { fsPath: "/repo" } }], + getConfiguration: () => ({ + get: (_key: string, value?: T) => value, + update: async () => {}, + }), + asRelativePath: (pathOrUri: string) => pathOrUri, + }, + window: { + activeTextEditor: undefined, + visibleTextEditors: [], + tabGroups: { all: [] }, + showTextDocument: async () => {}, + showWarningMessage: async () => undefined, + createTerminal: () => ({ show: noop, sendText: noop, dispose: noop }), + }, + commands: { + registerCommand: () => ({ dispose: noop }), + executeCommand: async () => {}, + }, + CodeAction: class { + command?: { command: string; title: string } + isPreferred?: boolean + constructor(public title: string, public kind: { value: string }) {} + }, + CodeActionKind: { + QuickFix: kind("quickfix"), + RefactorRewrite: kind("refactor.rewrite"), + }, + ConfigurationTarget: { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, + }, + TabInputText: class { + constructor(public uri: { scheme: string; fsPath: string }) {} + }, + Position: class { + constructor(public line: number, public character: number) {} + }, + Range: class { + constructor(public start: { line: number; character: number }, public end: { line: number; character: number }) {} + }, + Disposable: class { + constructor(private callback: () => void = noop) {} + dispose() { + this.callback() + } + }, + EventEmitter: class { + event = noop + fire = noop + dispose = noop + }, + TreeItem: class { + constructor(public label: string) {} + }, + TreeItemCollapsibleState: { None: 0, Collapsed: 1, Expanded: 2 }, + ViewColumn: { One: 1, Two: 2, Three: 3 }, +} + +mock.module("vscode", () => mockVscode) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 799c213fd57..483910f0e7a 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -24,6 +24,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/FileTree.tsx"), path.join(ROOT, "webview-ui/agent-manager/review-annotations.ts"), path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"), + path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"), ] const TSX_FILE = TSX_FILES[0] const PROVIDER_FILE = path.join(ROOT, "src/agent-manager/AgentManagerProvider.ts") diff --git a/packages/kilo-vscode/tests/unit/apply-conflicts.test.ts b/packages/kilo-vscode/tests/unit/apply-conflicts.test.ts new file mode 100644 index 00000000000..2d4a0078259 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/apply-conflicts.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "bun:test" +import { groupApplyConflicts, mapApplyConflictReason } from "../../webview-ui/agent-manager/apply-conflicts" + +describe("apply-conflicts", () => { + it("groups conflict reasons by file and deduplicates reasons", () => { + const rows = groupApplyConflicts([ + { file: "src/a.ts", reason: "patch failed" }, + { file: "src/a.ts", reason: "patch failed" }, + { file: "src/a.ts", reason: "does not match index" }, + { file: "src/b.ts", reason: "cannot read the current contents" }, + { reason: "general conflict" }, + ]) + + expect(rows).toEqual([ + { file: "src/a.ts", reasons: ["patch failed", "does not match index"] }, + { file: "src/b.ts", reasons: ["cannot read the current contents"] }, + { file: undefined, reasons: ["general conflict"] }, + ]) + }) + + it("maps known git conflict strings to reason keys", () => { + expect(mapApplyConflictReason("error: src/a.ts: does not match index")).toBe("index") + expect(mapApplyConflictReason("error: src/a.ts: patch does not apply")).toBe("patch") + expect(mapApplyConflictReason("error: src/a.ts: patch failed")).toBe("patch") + expect(mapApplyConflictReason("error: cannot read the current contents of 'src/a.ts'")).toBe("contents") + expect(mapApplyConflictReason("some unexpected git error")).toBeUndefined() + }) +}) diff --git a/packages/kilo-vscode/tests/unit/code-action-provider.test.ts b/packages/kilo-vscode/tests/unit/code-action-provider.test.ts index 669210689db..639c8fd2a09 100644 --- a/packages/kilo-vscode/tests/unit/code-action-provider.test.ts +++ b/packages/kilo-vscode/tests/unit/code-action-provider.test.ts @@ -1,31 +1,6 @@ -import { describe, it, expect, mock } from "bun:test" - -const makeAction = (title: string, kind: string) => ({ title, kind }) -const makeKind = (value: string) => ({ - value, - append: (v: string) => makeKind(`${value}.${v}`), -}) - -const QuickFix = makeKind("quickfix") -const RefactorRewrite = makeKind("refactor.rewrite") - -const mockVscode = { - CodeAction: class { - command?: { command: string; title: string } - isPreferred?: boolean - constructor( - public title: string, - public kind: { value: string }, - ) {} - }, - CodeActionKind: { - QuickFix, - RefactorRewrite, - }, -} - -mock.module("vscode", () => mockVscode) +import { describe, it, expect } from "bun:test" +// vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) const { KiloCodeActionProvider } = await import("../../src/services/code-actions/code-action-provider") const provider = new KiloCodeActionProvider() diff --git a/packages/kilo-vscode/tests/unit/git-ops.test.ts b/packages/kilo-vscode/tests/unit/git-ops.test.ts index 8ed99789528..8022c73c6db 100644 --- a/packages/kilo-vscode/tests/unit/git-ops.test.ts +++ b/packages/kilo-vscode/tests/unit/git-ops.test.ts @@ -1,4 +1,7 @@ import { describe, it, expect } from "bun:test" +import * as fs from "fs/promises" +import * as os from "os" +import * as nodePath from "path" import { GitOps } from "../../src/agent-manager/GitOps" function sleep(ms: number): Promise { @@ -9,6 +12,33 @@ function ops(handler: (args: string[], cwd: string) => Promise): GitOps return new GitOps({ log: () => undefined, refreshMs: 120000, runGit: handler }) } +function runGit(cwd: string, args: string[]): string { + const result = Bun.spawnSync({ + cmd: ["git", ...args], + cwd, + stderr: "pipe", + stdout: "pipe", + env: { + ...process.env, + GIT_TERMINAL_PROMPT: "0", + }, + }) + if (result.exitCode !== 0) { + throw new Error(Buffer.from(result.stderr).toString("utf8") || Buffer.from(result.stdout).toString("utf8")) + } + return Buffer.from(result.stdout).toString("utf8").trim() +} + +async function withRepo(run: (cwd: string) => Promise): Promise { + const cwd = await fs.mkdtemp(nodePath.join(os.tmpdir(), "kilo-gitops-test-")) + try { + runGit(cwd, ["init"]) + await run(cwd) + } finally { + await fs.rm(cwd, { recursive: true, force: true }) + } +} + describe("GitOps", () => { describe("currentBranch", () => { it("returns the current branch name", async () => { @@ -174,7 +204,7 @@ describe("GitOps", () => { await git.refreshRemote("/repo", "origin") const fetches = commands.filter((c) => c[0] === "fetch") expect(fetches.length).toBe(1) - expect(fetches[0][3]).toBe("origin") + expect(fetches[0]![3]).toBe("origin") }) it("skips empty remote name", async () => { @@ -221,8 +251,8 @@ describe("GitOps", () => { }) }) - describe("countMissingOriginCommits", () => { - it("counts commits ahead of upstream", async () => { + describe("aheadBehind", () => { + it("counts commits ahead and behind upstream", async () => { const git = ops(async (args) => { if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") return "origin/main" if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main" @@ -230,10 +260,10 @@ describe("GitOps", () => { if (args[0] === "config") return "origin" if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "3" + if (args[0] === "rev-list" && args[1] === "--left-right") return "1\t3" return "" }) - expect(await git.countMissingOriginCommits("/repo", "main")).toBe(3) + expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 3, behind: 1 }) }) it("uses resolved remote for non-origin setups", async () => { @@ -257,12 +287,12 @@ describe("GitOps", () => { ) return "abc" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "4" + if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t4" return "" }) - expect(await git.countMissingOriginCommits("/repo", "main")).toBe(4) + expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 4, behind: 0 }) const fetches = commands.filter((c) => c[0] === "fetch") - expect(fetches[0][3]).toBe("myfork") + expect(fetches[0]![3]).toBe("myfork") }) it("falls back to remote/parentBranch when no upstream and no remote branch", async () => { @@ -288,13 +318,13 @@ describe("GitOps", () => { ) return "abc" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "2" + if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t2" return "" }) - expect(await git.countMissingOriginCommits("/repo", "main")).toBe(2) + expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 2, behind: 0 }) }) - it("returns zero when rev-list fails", async () => { + it("returns zeros when rev-list fails", async () => { const git = ops(async (args) => { if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") return "origin/main" if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main" @@ -305,7 +335,107 @@ describe("GitOps", () => { if (args[0] === "rev-list") throw new Error("fatal") return "" }) - expect(await git.countMissingOriginCommits("/repo", "main")).toBe(0) + expect(await git.aheadBehind("/repo", "main")).toEqual({ ahead: 0, behind: 0 }) + }) + }) + + describe("buildWorktreePatch", () => { + it("includes tracked and untracked changes", async () => { + await withRepo(async (cwd) => { + const git = new GitOps({ log: () => undefined }) + await fs.writeFile(nodePath.join(cwd, "a.txt"), "one\n", "utf8") + await fs.writeFile(nodePath.join(cwd, "b.txt"), "one\n", "utf8") + runGit(cwd, ["add", "-A"]) + runGit(cwd, ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "init"]) + + await fs.writeFile(nodePath.join(cwd, "a.txt"), "two\n", "utf8") + await fs.writeFile(nodePath.join(cwd, "b.txt"), "two\n", "utf8") + await fs.writeFile(nodePath.join(cwd, "c.txt"), "new\n", "utf8") + + const branch = runGit(cwd, ["branch", "--show-current"]) || "HEAD" + const patch = await git.buildWorktreePatch(cwd, branch) + + expect(patch).toContain("a/a.txt") + expect(patch).toContain("a/b.txt") + expect(patch).toContain("a/c.txt") + }) + }) + + it("limits patch to selected files", async () => { + await withRepo(async (cwd) => { + const git = new GitOps({ log: () => undefined }) + await fs.writeFile(nodePath.join(cwd, "a.txt"), "one\n", "utf8") + await fs.writeFile(nodePath.join(cwd, "b.txt"), "one\n", "utf8") + runGit(cwd, ["add", "-A"]) + runGit(cwd, ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "init"]) + + await fs.writeFile(nodePath.join(cwd, "a.txt"), "two\n", "utf8") + await fs.writeFile(nodePath.join(cwd, "b.txt"), "two\n", "utf8") + await fs.writeFile(nodePath.join(cwd, "c.txt"), "new\n", "utf8") + + const branch = runGit(cwd, ["branch", "--show-current"]) || "HEAD" + const patch = await git.buildWorktreePatch(cwd, branch, ["a.txt", "c.txt"]) + + expect(patch).toContain("a/a.txt") + expect(patch).toContain("a/c.txt") + expect(patch).not.toContain("a/b.txt") + }) + }) + }) + + describe("checkApplyPatch", () => { + it("returns ok for a clean patch", async () => { + await withRepo(async (cwd) => { + const git = new GitOps({ log: () => undefined }) + await fs.writeFile(nodePath.join(cwd, "a.txt"), "one\n", "utf8") + runGit(cwd, ["add", "-A"]) + runGit(cwd, ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "init"]) + + await fs.writeFile(nodePath.join(cwd, "a.txt"), "two\n", "utf8") + const branch = runGit(cwd, ["branch", "--show-current"]) || "HEAD" + const patch = await git.buildWorktreePatch(cwd, branch) + + // Reset the file so the patch can apply cleanly to the original state + runGit(cwd, ["checkout", "--", "a.txt"]) + const result = await git.checkApplyPatch(cwd, patch) + expect(result.ok).toBe(true) + }) + }) + + it("returns not-ok for an empty patch", async () => { + const git = new GitOps({ log: () => undefined }) + const result = await git.checkApplyPatch("/tmp", "") + expect(result.ok).toBe(true) + expect(result.message).toBe("No changes to apply") + }) + }) + + describe("applyPatch", () => { + it("applies changes to the working tree", async () => { + await withRepo(async (cwd) => { + const git = new GitOps({ log: () => undefined }) + await fs.writeFile(nodePath.join(cwd, "a.txt"), "one\n", "utf8") + runGit(cwd, ["add", "-A"]) + runGit(cwd, ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "init"]) + + await fs.writeFile(nodePath.join(cwd, "a.txt"), "two\n", "utf8") + const branch = runGit(cwd, ["branch", "--show-current"]) || "HEAD" + const patch = await git.buildWorktreePatch(cwd, branch) + + runGit(cwd, ["checkout", "--", "a.txt"]) + const result = await git.applyPatch(cwd, patch) + expect(result.ok).toBe(true) + + const content = await fs.readFile(nodePath.join(cwd, "a.txt"), "utf8") + expect(content).toBe("two\n") + }) + }) + + it("returns empty patch as success", async () => { + const git = new GitOps({ log: () => undefined }) + const result = await git.applyPatch("/tmp", "") + expect(result.ok).toBe(true) + expect(result.message).toBe("No changes to apply") }) }) }) diff --git a/packages/kilo-vscode/tests/unit/git-stats-poller.test.ts b/packages/kilo-vscode/tests/unit/git-stats-poller.test.ts index eaebfd212a7..9781d04004f 100644 --- a/packages/kilo-vscode/tests/unit/git-stats-poller.test.ts +++ b/packages/kilo-vscode/tests/unit/git-stats-poller.test.ts @@ -65,7 +65,7 @@ describe("GitStatsPoller", () => { if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git" if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "1" + if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t1" return "" }), }) @@ -79,7 +79,9 @@ describe("GitStatsPoller", () => { it("keeps last-known stats when a later poll fails", async () => { let calls = 0 - const emitted: Array> = [] + const emitted: Array< + Array<{ worktreeId: string; files: number; additions: number; deletions: number; ahead: number; behind: number }> + > = [] const client = { worktree: { @@ -103,7 +105,7 @@ describe("GitStatsPoller", () => { if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git" if (args[0] === "rev-parse" && args[3] === "@{upstream}") return "origin/main" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "2" + if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t2" return "" }), }) @@ -115,16 +117,23 @@ describe("GitStatsPoller", () => { expect(emitted.length).toBeGreaterThan(0) const first = emitted[0] if (!first) throw new Error("expected emitted stats") - expect(first[0]).toEqual({ worktreeId: "a", additions: 7, deletions: 3, commits: 2 }) + expect(first[0]).toEqual({ worktreeId: "a", files: 1, additions: 7, deletions: 3, ahead: 2, behind: 0 }) const hasZeros = emitted.some((batch) => - batch.some((item) => item.additions === 0 && item.deletions === 0 && item.commits === 0), + batch.some((item) => item.additions === 0 && item.deletions === 0 && item.ahead === 0), ) expect(hasZeros).toBe(false) }) it("preserves local stats when client fails after initial success", async () => { let diffCalls = 0 - const emitted: Array<{ branch: string; additions: number; deletions: number; commits: number }> = [] + const emitted: Array<{ + branch: string + files: number + additions: number + deletions: number + ahead: number + behind: number + }> = [] const client = { worktree: { @@ -149,7 +158,7 @@ describe("GitStatsPoller", () => { if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") return "origin/feature" if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "3" + if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t3" if (args[0] === "branch") return "feature" if (args[0] === "config") return "origin" return "" @@ -161,12 +170,19 @@ describe("GitStatsPoller", () => { poller.stop() expect(emitted.length).toBeGreaterThan(0) - expect(emitted[0]).toEqual({ branch: "feature", additions: 5, deletions: 2, commits: 3 }) + expect(emitted[0]).toEqual({ branch: "feature", files: 1, additions: 5, deletions: 2, ahead: 3, behind: 0 }) expect(emitted.length).toBe(1) }) it("falls back to /HEAD when no upstream and no /", async () => { - const emitted: Array<{ branch: string; additions: number; deletions: number; commits: number }> = [] + const emitted: Array<{ + branch: string + files: number + additions: number + deletions: number + ahead: number + behind: number + }> = [] const client = { worktree: { diff: async () => ({ data: diff(10, 4) }) }, @@ -196,7 +212,7 @@ describe("GitStatsPoller", () => { if (args[0] === "branch") return "my-feature" if (args[0] === "rev-parse" && args[1] === "--git-common-dir") return ".git" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "5" + if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t5" return "" }), }) @@ -205,11 +221,18 @@ describe("GitStatsPoller", () => { await waitFor(() => emitted.length >= 1) poller.stop() - expect(emitted[0]).toEqual({ branch: "my-feature", additions: 10, deletions: 4, commits: 5 }) + expect(emitted[0]).toEqual({ branch: "my-feature", files: 1, additions: 10, deletions: 4, ahead: 5, behind: 0 }) }) - it("emits zeros when no tracking, no default branch, and no remote refs exist", async () => { - const emitted: Array<{ branch: string; additions: number; deletions: number; commits: number }> = [] + it("falls back to workingTreeStats when no tracking, no default branch, and no remote refs exist", async () => { + const emitted: Array<{ + branch: string + files: number + additions: number + deletions: number + ahead: number + behind: number + }> = [] const client = { worktree: { diff: async () => ({ data: diff(0, 0) }) }, @@ -231,6 +254,9 @@ describe("GitStatsPoller", () => { throw new Error("no ref") if (args[0] === "symbolic-ref") throw new Error("no symbolic ref") if (args[0] === "rev-parse" && args[1] === "--verify" && args[2] === "--quiet") throw new Error("no ref") + // workingTreeStats fallback: no tracked changes, no untracked files + if (args[0] === "diff") return "" + if (args[0] === "ls-files") return "" return "" }), }) @@ -239,12 +265,21 @@ describe("GitStatsPoller", () => { await waitFor(() => emitted.length >= 1) poller.stop() - expect(emitted[0]).toEqual({ branch: "orphan-branch", additions: 0, deletions: 0, commits: 0 }) + expect(emitted[0]).toEqual({ + branch: "orphan-branch", + files: 0, + additions: 0, + deletions: 0, + ahead: 0, + behind: 0, + }) }) it("refreshes upstream remote once for concurrent worktrees", async () => { const commands: string[][] = [] - const emitted: Array> = [] + const emitted: Array< + Array<{ worktreeId: string; files: number; additions: number; deletions: number; ahead: number; behind: number }> + > = [] const client = { worktree: { diff: async () => ({ data: diff(0, 0) }) }, @@ -265,7 +300,7 @@ describe("GitStatsPoller", () => { if (args[0] === "branch") return "feature" if (args[0] === "config") return "origin" if (args[0] === "fetch") return "" - if (args[0] === "rev-list") return "0" + if (args[0] === "rev-list" && args[1] === "--left-right") return "0\t0" return "" }), }) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts index a5909fdaad7..70768777eca 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts @@ -1,55 +1,6 @@ -import { describe, it, expect, mock } from "bun:test" - -const kind = (value: string) => ({ - value, - append: (part: string) => kind(`${value}.${part}`), -}) - -const mockVscode = { - Uri: { - joinPath: (...args: unknown[]) => ({ fsPath: args.join("/") }), - file: (p: string) => ({ fsPath: p }), - }, - Disposable: class { - constructor(public callOnDispose: () => void) {} - dispose() { - this.callOnDispose() - } - }, - extensions: { - getExtension: () => ({ - packageJSON: { version: "test" }, - }), - }, - env: { - appName: "VS Code", - language: "en", - machineId: "machine", - isTelemetryEnabled: false, - }, - version: "1.0.0", - workspace: { - workspaceFolders: [{ uri: { fsPath: "/repo" } }], - getConfiguration: () => ({ - get: (_key: string, value?: T) => value, - }), - }, - CodeAction: class { - command?: { command: string; title: string } - isPreferred?: boolean - constructor( - public title: string, - public kind: { value: string }, - ) {} - }, - CodeActionKind: { - QuickFix: kind("quickfix"), - RefactorRewrite: kind("refactor.rewrite"), - }, -} - -mock.module("vscode", () => mockVscode) +import { describe, it, expect } from "bun:test" +// vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) const { KiloProvider } = await import("../../src/KiloProvider") type State = "connecting" | "connected" | "disconnected" | "error" @@ -112,7 +63,7 @@ function createConnection(client: ReturnType) { } describe("KiloProvider pending session refresh", () => { - it.skip("flushes deferred refresh in initializeConnection without relying on connected event callback", async () => { + it("flushes deferred refresh in initializeConnection without relying on connected event callback", async () => { const client = createClient() const connection = createConnection(client) const provider = new KiloProvider({} as never, connection as never) @@ -129,7 +80,7 @@ describe("KiloProvider pending session refresh", () => { expect(internal.pendingSessionRefresh).toBe(false) }) - it.skip("does not post not-connected errors while still connecting", async () => { + it("does not post not-connected errors while still connecting", async () => { const client = createClient() const connection = createConnection(client) const provider = new KiloProvider({} as never, connection as never) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 1d65ef023b6..9d58a37114b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -7,6 +7,7 @@ import { createSignal, createMemo, createEffect, + on, onMount, onCleanup, type Accessor, @@ -23,6 +24,9 @@ import type { AgentManagerImportResultMessage, AgentManagerWorktreeDiffMessage, AgentManagerWorktreeDiffLoadingMessage, + AgentManagerApplyWorktreeDiffResultMessage, + AgentManagerApplyWorktreeDiffStatus, + AgentManagerApplyWorktreeDiffConflict, AgentManagerWorktreeStatsMessage, AgentManagerLocalStatsMessage, WorktreeFileDiff, @@ -78,6 +82,8 @@ import { reorderTabs, applyTabOrder, firstOrderedTitle } from "./tab-order" import { ConstrainDragYAxis, SortableReviewTab, SortableTab } from "./sortable-tab" import { DiffPanel } from "./DiffPanel" import { FullScreenDiffView } from "./FullScreenDiffView" +import { ApplyDialog } from "./ApplyDialog" +import { groupApplyConflicts } from "./apply-conflicts" import type { ReviewComment } from "./review-comments" import "./agent-manager.css" @@ -97,6 +103,12 @@ interface WorktreeBusyState { branch?: string } +interface ApplyState { + status: AgentManagerApplyWorktreeDiffStatus + message: string + conflicts: AgentManagerApplyWorktreeDiffConflict[] +} + /** Sidebar selection: LOCAL for workspace, worktree ID for a worktree, or null for an unassigned session. */ type SidebarSelection = typeof LOCAL | string | null @@ -325,6 +337,12 @@ const AgentManagerContent: Component = () => { // Local workspace git stats (branch name, diff additions/deletions, commits) const [localStats, setLocalStats] = createSignal() + // Per-worktree apply-to-local status + const [applyStates, setApplyStates] = createSignal>({}) + const [applyTarget, setApplyTarget] = createSignal() + const [applySelectedFiles, setApplySelectedFiles] = createSignal([]) + const [applySelectionTouched, setApplySelectionTouched] = createSignal(false) + // Pending local tab counter for generating unique IDs let pendingCounter = 0 const PENDING_PREFIX = "pending:" @@ -364,6 +382,197 @@ const AgentManagerContent: Component = () => { setReviewCommentsByWorktree((prev) => ({ ...prev, [sel]: comments })) } + const applyStateForSelection = createMemo(() => { + const sel = selection() + if (!sel || sel === LOCAL) return undefined + return applyStates()[sel] + }) + + const resolveWorktreeSessionId = (worktreeId: string) => { + const id = session.currentSessionID() + if (id) { + const current = managedSessions().find((entry) => entry.id === id) + if (current?.worktreeId === worktreeId) return id + } + return managedSessions().find((entry) => entry.worktreeId === worktreeId)?.id + } + + const applyTargetSessionId = createMemo(() => { + const target = applyTarget() + if (!target) return undefined + return resolveWorktreeSessionId(target) + }) + + const applyDiffs = createMemo(() => { + const target = applyTarget() + if (!target) return [] as WorktreeFileDiff[] + const data = diffDatas() + const current = applyTargetSessionId() + if (current && data[current]) return data[current]! + const ids = managedSessions() + .filter((entry) => entry.worktreeId === target) + .map((entry) => entry.id) + for (const id of ids) { + if (data[id]) return data[id]! + } + return [] as WorktreeFileDiff[] + }) + + const applyStateForTarget = createMemo(() => { + const target = applyTarget() + if (!target) return undefined + return applyStates()[target] + }) + + const applyBusyForTarget = createMemo(() => { + const state = applyStateForTarget() + if (!state) return false + return state.status === "checking" || state.status === "applying" + }) + + const applySelectedSet = createMemo(() => new Set(applySelectedFiles())) + + const applySelectionStats = createMemo(() => { + const set = applySelectedSet() + const selected = applyDiffs().filter((diff) => set.has(diff.file)) + const additions = selected.reduce((sum, diff) => sum + diff.additions, 0) + const deletions = selected.reduce((sum, diff) => sum + diff.deletions, 0) + return { + total: applyDiffs().length, + selected: selected.length, + additions, + deletions, + } + }) + + const applyHasSelection = createMemo(() => applySelectionStats().selected > 0) + + const applyConflictRows = createMemo(() => groupApplyConflicts(applyStateForTarget()?.conflicts ?? [])) + + const applyToLocal = (worktreeId: string, selectedFiles: string[]) => { + setApplyStates((prev) => ({ + ...prev, + [worktreeId]: { + status: "checking", + message: t("agentManager.apply.checking"), + conflicts: [], + }, + })) + vscode.postMessage({ type: "agentManager.applyWorktreeDiff", worktreeId, selectedFiles }) + } + + const resetApplyDialog = () => { + setApplyTarget(undefined) + setApplySelectedFiles([]) + setApplySelectionTouched(false) + } + + const closeApplyDialog = () => { + resetApplyDialog() + dialog.close() + } + + const applySelectAll = () => { + setApplySelectionTouched(true) + setApplySelectedFiles(applyDiffs().map((diff) => diff.file)) + } + + const applySelectNone = () => { + setApplySelectionTouched(true) + setApplySelectedFiles([]) + } + + const applyToggleFile = (file: string, checked: boolean) => { + setApplySelectionTouched(true) + setApplySelectedFiles((prev) => { + if (checked) { + if (prev.includes(file)) return prev + const set = new Set(prev) + set.add(file) + return applyDiffs() + .map((diff) => diff.file) + .filter((path) => set.has(path)) + } + if (!prev.includes(file)) return prev + return prev.filter((path) => path !== file) + }) + } + + const triggerApply = () => { + const target = applyTarget() + if (!target) return + if (!applyHasSelection()) return + if (applyBusyForTarget()) return + applyToLocal(target, applySelectedFiles()) + } + + const openApplyDialog = () => { + const sel = selection() + if (!sel || sel === LOCAL) return + setApplyStates((prev) => { + if (!prev[sel]) return prev + const next = { ...prev } + delete next[sel] + return next + }) + setApplyTarget(sel) + setApplySelectionTouched(false) + setApplySelectedFiles([]) + const sid = resolveWorktreeSessionId(sel) + if (sid) vscode.postMessage({ type: "agentManager.requestWorktreeDiff", sessionId: sid }) + + setApplySelectedFiles(applyDiffs().map((diff) => diff.file)) + + dialog.show( + () => ( + + ), + resetApplyDialog, + ) + } + + createEffect( + on( + () => [applyTarget(), applyDiffs(), applySelectionTouched()] as const, + ([target, diffs, touched]) => { + if (!target) return + const files = diffs.map((diff) => diff.file) + if (files.length === 0) { + if (!touched) setApplySelectedFiles([]) + return + } + + if (!touched) { + setApplySelectedFiles(files) + return + } + + const current = applySelectedFiles() + const set = new Set(current) + const next = files.filter((file) => set.has(file)) + const same = next.length === current.length && next.every((file, index) => file === current[index]) + if (!same) setApplySelectedFiles(next) + }, + ), + ) + const isPending = (id: string) => id.startsWith(PENDING_PREFIX) // Drag-and-drop state for tab reordering @@ -424,6 +633,15 @@ const AgentManagerContent: Component = () => { if (Object.keys(next).length === Object.keys(prev).length) return prev return next }) + + setApplyStates((prev) => { + const next = Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))) + if (Object.keys(next).length === Object.keys(prev).length) return prev + return next + }) + + const target = applyTarget() + if (target && !ids.has(target)) closeApplyDialog() }) const worktreeSessionIds = createMemo( @@ -965,6 +1183,33 @@ const AgentManagerContent: Component = () => { setDiffLoading(ev.loading) } + if (msg.type === "agentManager.applyWorktreeDiffResult") { + const ev = msg as AgentManagerApplyWorktreeDiffResultMessage + const files = new Set((ev.conflicts ?? []).map((entry) => entry.file).filter(Boolean)).size + const count = ev.conflicts?.length ?? 0 + setApplyStates((prev) => ({ + ...prev, + [ev.worktreeId]: { + status: ev.status, + message: ev.message, + conflicts: ev.conflicts ?? [], + }, + })) + + if (ev.status === "success") { + showToast({ variant: "success", title: t("agentManager.apply.success"), description: ev.message }) + if (applyTarget() === ev.worktreeId) closeApplyDialog() + } + if (ev.status === "conflict") { + const summary = + count > 0 ? t("agentManager.apply.conflictToast", { count, files: Math.max(files, 1) }) : ev.message + showToast({ variant: "error", title: t("agentManager.apply.conflict"), description: summary }) + } + if (ev.status === "error") { + showToast({ variant: "error", title: t("agentManager.apply.error"), description: ev.message }) + } + } + if (msg.type === "agentManager.worktreeStats") { const ev = msg as AgentManagerWorktreeStatsMessage const map: Record = {} @@ -1394,10 +1639,18 @@ const AgentManagerContent: Component = () => { 0 || localStats()!.deletions > 0 || localStats()!.commits > 0) + localStats() && + (localStats()!.files > 0 || + localStats()!.additions > 0 || + localStats()!.deletions > 0 || + localStats()!.ahead > 0 || + localStats()!.behind > 0) } >
+ 0}> + {localStats()!.files}f + 0 || localStats()!.deletions > 0}> 0}> @@ -1411,10 +1664,16 @@ const AgentManagerContent: Component = () => { - 0}> + 0}> {"↑"} - {localStats()!.commits} + {localStats()!.ahead} + + + 0}> + + {"↓"} + {localStats()!.behind}
@@ -1646,10 +1905,17 @@ const AgentManagerContent: Component = () => { 0 || stats()!.deletions > 0 || stats()!.commits > 0) + (stats()!.files > 0 || + stats()!.additions > 0 || + stats()!.deletions > 0 || + stats()!.ahead > 0 || + stats()!.behind > 0) } >
+ 0}> + {stats()!.files}f + 0 || stats()!.deletions > 0}> 0}> @@ -1660,8 +1926,11 @@ const AgentManagerContent: Component = () => { - 0}> - ↑{stats()!.commits} + 0}> + ↑{stats()!.ahead} + + 0}> + ↓{stats()!.behind}
@@ -1721,12 +1990,22 @@ const AgentManagerContent: Component = () => { 0 || + (hoverStats()!.files > 0 || + hoverStats()!.additions > 0 || hoverStats()!.deletions > 0 || - hoverStats()!.commits > 0) + hoverStats()!.ahead > 0 || + hoverStats()!.behind > 0) } >
+ 0}> +
+ + {t("agentManager.hoverCard.files")} + + {hoverStats()!.files} +
+
0 || hoverStats()!.deletions > 0}>
@@ -1742,12 +2021,19 @@ const AgentManagerContent: Component = () => {
- 0}> + 0 || hoverStats()!.behind > 0}>
{t("agentManager.hoverCard.commits")} - {hoverStats()!.commits} + + 0}> + ↑{hoverStats()!.ahead} + + 0}> + ↓{hoverStats()!.behind} + +
@@ -1951,41 +2237,67 @@ const AgentManagerContent: Component = () => {
{(() => { const sel = () => selection() + const isWorktree = () => typeof sel() === "string" && sel() !== LOCAL const stats = () => { if (sel() === LOCAL) return localStats() return typeof sel() === "string" ? worktreeStats()[sel() as string] : undefined } const hasChanges = () => { const s = stats() - return s && (s.additions > 0 || s.deletions > 0) + return s && (s.files > 0 || s.additions > 0 || s.deletions > 0) + } + const applyBusy = () => { + const state = applyStateForSelection() + if (!state) return false + return state.status === "checking" || state.status === "applying" } return ( - - + + + - - - - +{stats()!.additions} - −{stats()!.deletions} - - - - + + + ) })()} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ApplyDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ApplyDialog.tsx new file mode 100644 index 00000000000..cbce7dbb7ab --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/ApplyDialog.tsx @@ -0,0 +1,144 @@ +import { type Component, For, Show } from "solid-js" +import { Dialog } from "@kilocode/kilo-ui/dialog" +import { Button } from "@kilocode/kilo-ui/button" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import type { AgentManagerApplyWorktreeDiffStatus, WorktreeFileDiff } from "../src/types/messages" +import { useLanguage } from "../src/context/language" +import { FileTree } from "./FileTree" +import { mapApplyConflictReason, type ApplyConflictRow } from "./apply-conflicts" + +interface ApplyDialogProps { + diffs: WorktreeFileDiff[] + loading: boolean + selectedFiles: Set + selectedCount: number + additions: number + deletions: number + busy: boolean + hasSelection: boolean + status?: AgentManagerApplyWorktreeDiffStatus + message?: string + conflictRows: ApplyConflictRow[] + onSelectAll: () => void + onSelectNone: () => void + onToggleFile: (file: string, checked: boolean) => void + onApply: () => void + onClose: () => void +} + +export const ApplyDialog: Component = (props) => { + const { t } = useLanguage() + + const conflictReason = (reason: string) => { + const mapped = mapApplyConflictReason(reason) + if (mapped === "index") return t("agentManager.apply.reason.index") + if (mapped === "patch") return t("agentManager.apply.reason.patch") + if (mapped === "contents") return t("agentManager.apply.reason.contents") + return reason || t("agentManager.apply.reason.unknown") + } + + return ( + +
{ + if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { + e.preventDefault() + e.stopPropagation() + props.onApply() + return + } + if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + props.onClose() + } + }} + > +
+
+ {t("agentManager.apply.selectionSummary", { + selected: props.selectedCount, + total: props.diffs.length, + })} + +{props.additions} + -{props.deletions} +
+
+ + +
+
+ +
+ 0} + fallback={ +
+ {props.loading ? t("session.review.loadingChanges") : t("session.review.noChanges")} +
+ } + > + undefined} + selectedFiles={props.selectedFiles} + onFileToggle={props.onToggleFile} + showSummary={false} + /> +
+
+ + 0}> +
+
{t("agentManager.apply.conflictsTitle")}
+ +
{props.message}
+
+ + {(entry) => ( +
+ {entry.file ?? t("agentManager.apply.unknownFile")} + {conflictReason(entry.reasons[0] ?? "")} +
+ )} +
+
+
+ + +
{props.message}
+
+ + +
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FileTree.tsx b/packages/kilo-vscode/webview-ui/agent-manager/FileTree.tsx index 855ed4787ba..ea5a29646d7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FileTree.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/FileTree.tsx @@ -14,6 +14,9 @@ interface FileTreeProps { activeFile: string | null onFileSelect: (path: string) => void comments?: ReviewComment[] + selectedFiles?: Set + onFileToggle?: (path: string, checked: boolean) => void + showSummary?: boolean } const DirectoryNode: Component<{ @@ -22,6 +25,8 @@ const DirectoryNode: Component<{ onFileSelect: (path: string) => void depth: number commentsByFile?: Map + selectedFiles?: Set + onFileToggle?: (path: string, checked: boolean) => void }> = (props) => { const [expanded, setExpanded] = createSignal(true) const hasActiveDescendant = createMemo(() => { @@ -52,6 +57,8 @@ const DirectoryNode: Component<{ onFileSelect={props.onFileSelect} depth={props.depth + 1} commentsByFile={props.commentsByFile} + selectedFiles={props.selectedFiles} + onFileToggle={props.onFileToggle} /> } > @@ -61,6 +68,8 @@ const DirectoryNode: Component<{ onFileSelect={props.onFileSelect} depth={props.depth + 1} commentsByFile={props.commentsByFile} + selectedFiles={props.selectedFiles} + onFileToggle={props.onFileToggle} />
)} @@ -76,8 +85,12 @@ const FileNode: Component<{ onFileSelect: (path: string) => void depth: number commentsByFile?: Map + selectedFiles?: Set + onFileToggle?: (path: string, checked: boolean) => void }> = (props) => { const active = () => props.activeFile === props.node.path + const checked = () => props.selectedFiles?.has(props.node.path) ?? false + const selectable = () => Boolean(props.onFileToggle) const status = () => props.node.diff?.status ?? "modified" const additions = () => props.node.diff?.additions ?? 0 const deletions = () => props.node.diff?.deletions ?? 0 @@ -89,13 +102,27 @@ const FileNode: Component<{
-
- {t("session.review.filesChanged", { count: totals().files })} - +{totals().additions} - -{totals().deletions} -
+ +
+ {t("session.review.filesChanged", { count: totals().files })} + +{totals().additions} + -{totals().deletions} +
+
) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 2dd907aac74..18d0f798fba 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -61,6 +61,10 @@ color: var(--text-weak); } +.am-local-item-active .am-stat-files { + color: rgba(255, 255, 255, 0.7); +} + .am-local-icon { width: 18px; height: 18px; @@ -73,6 +77,7 @@ flex-direction: column; min-width: 0; gap: 0; + flex: 1; } .am-local-label { @@ -322,6 +327,10 @@ button.am-section-toggle:hover .am-section-label { background: rgba(255, 255, 255, 0.12); } +.am-stat-files { + color: var(--text-muted); +} + .am-stat-additions { color: #34d399; } @@ -330,6 +339,10 @@ button.am-section-toggle:hover .am-section-label { color: #f87171; } +.am-worktree-item-active .am-stat-files { + color: rgba(255, 255, 255, 0.7); +} + .am-worktree-item-active .am-stat-additions { color: #6ee7b7; } @@ -342,11 +355,24 @@ button.am-section-toggle:hover .am-section-label { color: #34d399; } +.am-worktree-behind { + color: #f87171; +} + .am-worktree-item-active .am-worktree-commits { color: #6ee7b7; } -.am-worktree-item:hover .am-worktree-stats { +.am-worktree-item-active .am-worktree-behind { + color: #fca5a5; +} + +.am-local-item-active .am-worktree-behind { + color: #fca5a5; +} + +.am-worktree-item:hover .am-worktree-stats, +.am-local-item:hover .am-worktree-stats { opacity: 0; } @@ -756,6 +782,10 @@ button.am-section-toggle:hover .am-section-label { gap: 4px; } +.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-files { + color: rgba(255, 255, 255, 0.7); +} + .am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-additions { color: #6ee7b7; } @@ -887,6 +917,7 @@ button.am-section-toggle:hover .am-section-label { display: flex; align-items: center; gap: 10px; + flex: 1; min-width: 0; } @@ -2427,13 +2458,127 @@ button.am-section-toggle:hover .am-section-label { .am-diff-header-actions { display: flex; align-items: center; - gap: 2px; + gap: 6px; + margin-left: 8px; + min-width: 0; } .am-diff-header-actions > * { flex-shrink: 0; } +.am-apply-spinner { + width: 12px; + height: 12px; +} + +.am-apply-dialog { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + height: 100%; + min-height: 0; + padding: 0 20px 20px 20px; + box-sizing: border-box; +} + +.am-apply-dialog-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; +} + +.am-apply-dialog-summary { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-weak); + font-variant-numeric: tabular-nums; +} + +.am-apply-dialog-actions { + display: inline-flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.am-apply-dialog-tree { + flex: 1; + min-height: 0; + border: 1px solid var(--border-weak-base); + border-radius: var(--radius-sm); + overflow: hidden; +} + +.am-apply-dialog-tree .am-file-tree { + height: 100%; +} + +.am-apply-conflicts { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px; + border: 1px solid color-mix(in oklab, var(--border-weak-base) 55%, var(--syntax-diff-delete, #da3319)); + border-radius: var(--radius-sm); + background: color-mix(in oklab, var(--surface-inset-base) 90%, var(--syntax-diff-delete, #da3319)); +} + +.am-apply-conflicts-title { + font-size: 11px; + font-weight: 600; + color: var(--text-base); +} + +.am-apply-conflict { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + min-width: 0; +} + +.am-apply-conflict-file { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + color: var(--text-base); +} + +.am-apply-conflict-reason { + flex: 1; + min-width: 0; + text-align: right; + line-height: 1.3; + white-space: normal; + word-break: break-word; + font-size: 10px; + color: var(--text-weaker); +} + +.am-apply-alert { + font-size: 11px; + color: var(--syntax-diff-delete, #da3319); + line-height: 1.35; +} + +.am-apply-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + border-top: 1px solid var(--border-weak-base); + padding-top: 10px; + flex-shrink: 0; +} + /* Review tab in tab bar */ .am-tab-review { @@ -2745,6 +2890,34 @@ button.am-section-toggle:hover .am-section-label { color: var(--text-base); } +.am-file-tree-check { + width: 14px; + height: 14px; + border: 1px solid var(--border-weak-base); + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: transparent; + background: var(--surface-base); +} + +.am-file-tree-check-on { + border-color: var(--surface-interactive-strong); + background: color-mix(in oklab, var(--surface-interactive-strong) 18%, var(--surface-base)); + color: var(--surface-interactive-strong); +} + +.am-file-tree-check [data-component="icon"] { + width: 10px; + height: 10px; +} + +.am-file-tree-selected { + background: color-mix(in oklab, var(--surface-base-active, rgba(128, 128, 128, 0.15)) 72%, transparent); +} + /* Compact +N -N stats pushed to the right */ .am-file-tree-changes { display: flex; diff --git a/packages/kilo-vscode/webview-ui/agent-manager/apply-conflicts.ts b/packages/kilo-vscode/webview-ui/agent-manager/apply-conflicts.ts new file mode 100644 index 00000000000..2c6149ec98a --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/apply-conflicts.ts @@ -0,0 +1,31 @@ +import type { AgentManagerApplyWorktreeDiffConflict } from "../src/types/messages" + +export interface ApplyConflictRow { + file?: string + reasons: string[] +} + +export function groupApplyConflicts(conflicts: AgentManagerApplyWorktreeDiffConflict[]): ApplyConflictRow[] { + const map = new Map }>() + + for (const conflict of conflicts) { + const file = conflict.file?.trim() + const key = file && file.length > 0 ? file : "__unknown__" + const row = map.get(key) + if (!row) { + map.set(key, { file, reasons: new Set([conflict.reason]) }) + continue + } + row.reasons.add(conflict.reason) + } + + return Array.from(map.values()).map((row) => ({ file: row.file, reasons: Array.from(row.reasons) })) +} + +export function mapApplyConflictReason(reason: string): "index" | "patch" | "contents" | undefined { + const text = reason.toLowerCase() + if (text.includes("does not match index")) return "index" + if (text.includes("patch does not apply") || text.includes("patch failed")) return "patch" + if (text.includes("cannot read the current contents")) return "contents" + return undefined +} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 96a12d0556e..e69c9d0bbd2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -3,7 +3,7 @@ * Text input with send/abort buttons, ghost-text autocomplete, and @ file mention support */ -import { Component, createSignal, createEffect, on, For, Index, onCleanup, Show, untrack } from "solid-js" +import { Component, createSignal, createEffect, on, For, Index, onCleanup, onMount, Show, untrack } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { FileIcon } from "@kilocode/kilo-ui/file-icon" @@ -16,6 +16,7 @@ import { ModeSwitcher } from "../shared/ModeSwitcher" import { ThinkingSelector } from "../shared/ThinkingSelector" import { useFileMention } from "../../hooks/useFileMention" import { useImageAttachments } from "../../hooks/useImageAttachments" +import { WandSparkles } from "@kilocode/kilo-ui/lucide" import { fileName, dirName, buildHighlightSegments } from "./prompt-input-utils" const AUTOCOMPLETE_DEBOUNCE_MS = 500 @@ -36,6 +37,10 @@ export const PromptInput: Component = () => { const [text, setText] = createSignal("") const [ghostText, setGhostText] = createSignal("") + const [chatAutocompleteEnabled, setChatAutocompleteEnabled] = createSignal(false) + const [enhancing, setEnhancing] = createSignal(false) + let enhanceCounter = 0 + let preEnhanceText: string | null = null let textareaRef: HTMLTextAreaElement | undefined let highlightRef: HTMLDivElement | undefined @@ -79,6 +84,10 @@ export const PromptInput: Component = () => { } } + if (message.type === "autocompleteSettingsLoaded") { + setChatAutocompleteEnabled(message.settings.enableChatAutocomplete) + } + if (message.type === "setChatBoxMessage") { setText(message.text) setGhostText("") @@ -111,6 +120,31 @@ export const PromptInput: Component = () => { if (message.type === "action" && message.action === "focusInput") { textareaRef?.focus() } + + if (message.type === "enhancePromptResult") { + const result = message as import("../../types/messages").EnhancePromptResultMessage + if (result.requestId === `enhance-${enhanceCounter}`) { + setText(result.text) + setGhostText("") + setEnhancing(false) + if (textareaRef) { + textareaRef.value = result.text + adjustHeight() + textareaRef.focus() + } + } + } + + if (message.type === "enhancePromptError") { + const result = message as import("../../types/messages").EnhancePromptErrorMessage + if (result.requestId === `enhance-${enhanceCounter}`) { + setEnhancing(false) + } + } + }) + + onMount(() => { + vscode.postMessage({ type: "requestAutocompleteSettings" }) }) onCleanup(() => { @@ -122,7 +156,7 @@ export const PromptInput: Component = () => { }) const requestAutocomplete = (val: string) => { - if (val.length < MIN_TEXT_LENGTH || isDisabled()) { + if (val.length < MIN_TEXT_LENGTH || isDisabled() || !chatAutocompleteEnabled()) { setGhostText("") return } @@ -181,6 +215,7 @@ export const PromptInput: Component = () => { const target = e.target as HTMLTextAreaElement const val = target.value setText(val) + preEnhanceText = null adjustHeight() setGhostText("") syncHighlightScroll() @@ -198,6 +233,20 @@ export const PromptInput: Component = () => { } const handleKeyDown = (e: KeyboardEvent) => { + // Undo enhanced prompt with Ctrl+Z / ⌘Z + if (e.key === "z" && (e.metaKey || e.ctrlKey) && !e.shiftKey && preEnhanceText !== null) { + e.preventDefault() + const restored = preEnhanceText + preEnhanceText = null + setText(restored) + setGhostText("") + if (textareaRef) { + textareaRef.value = restored + adjustHeight() + } + return + } + if (mention.onKeyDown(e, textareaRef, setText, adjustHeight)) { setGhostText("") queueMicrotask(scrollToActiveItem) @@ -228,6 +277,28 @@ export const PromptInput: Component = () => { } } + const canEnhance = () => !isBusy() && !isDisabled() && !enhancing() + + const handleEnhance = () => { + if (isDisabled() || enhancing() || isBusy()) return + const draft = text().trim() + if (!draft) { + const description = language.t("prompt.action.enhanceDescription") + setText(description) + setGhostText("") + if (textareaRef) { + textareaRef.value = description + adjustHeight() + textareaRef.focus() + } + return + } + preEnhanceText = text() + enhanceCounter++ + setEnhancing(true) + vscode.postMessage({ type: "enhancePrompt", text: draft, requestId: `enhance-${enhanceCounter}` }) + } + const handleSend = () => { const message = text().trim() const imgs = imageAttach.images() @@ -343,6 +414,17 @@ export const PromptInput: Component = () => {
+ + + } +// Enhance prompt result (extension → webview) +export interface EnhancePromptResultMessage { + type: "enhancePromptResult" + text: string + requestId: string +} + +// Enhance prompt error (extension → webview) +export interface EnhancePromptErrorMessage { + type: "enhancePromptError" + error: string + requestId: string +} + export type ExtensionMessage = | ReadyMessage | ConnectionStateMessage @@ -830,8 +863,11 @@ export type ExtensionMessage = | WorkspaceDirectoryChangedMessage | AgentManagerWorktreeDiffMessage | AgentManagerWorktreeDiffLoadingMessage + | AgentManagerApplyWorktreeDiffResultMessage | AgentManagerWorktreeStatsMessage | AgentManagerLocalStatsMessage + | EnhancePromptResultMessage + | EnhancePromptErrorMessage // ============================================ // Messages FROM webview TO extension @@ -1232,6 +1268,12 @@ export interface StopDiffWatchMessage { type: "agentManager.stopDiffWatch" } +export interface ApplyWorktreeDiffMessage { + type: "agentManager.applyWorktreeDiff" + worktreeId: string + selectedFiles?: string[] +} + // Variant persistence (webview → extension) export interface PersistVariantRequest { type: "persistVariant" @@ -1244,6 +1286,13 @@ export interface RequestVariantsMessage { type: "requestVariants" } +// Enhance prompt request (webview → extension) +export interface EnhancePromptRequest { + type: "enhancePrompt" + text: string + requestId: string +} + export type WebviewMessage = | SendMessageRequest | AbortRequest @@ -1315,6 +1364,8 @@ export type WebviewMessage = | RequestWorktreeDiffMessage | StartDiffWatchMessage | StopDiffWatchMessage + | ApplyWorktreeDiffMessage + | EnhancePromptRequest // ============================================ // VS Code API type diff --git a/packages/opencode/package.json b/packages/opencode/package.json index bf3b34d654a..efdcb0a4234 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.0.33", + "version": "7.0.35", "name": "@kilocode/cli", "type": "module", "license": "MIT", diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 9696d17add9..99976e1185a 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -1,7 +1,6 @@ import type { Argv } from "yargs" import type { Session as SDKSession, Message, Part } from "@kilocode/sdk/v2" import { Session } from "../../session" -import { Bus } from "../../bus" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" import { Database } from "../../storage/db" @@ -10,6 +9,9 @@ import { Instance } from "../../project/instance" import { ShareNext } from "../../share/share-next" import { EOL } from "os" import { Filesystem } from "../../util/filesystem" +import { Log } from "../../util/log" + +const log = Log.create({ service: "import-command" }) /** Discriminated union returned by the ShareNext API (GET /api/share/:id/data) */ export type ShareData = @@ -65,6 +67,44 @@ export function transformShareData(shareData: ShareData[]): { } } +// kilocode_change start +export function ingestBootstrapWarning(sessionId: string, error: unknown) { + const details = error instanceof Error ? error.message : String(error) + return `Warning: imported session ${sessionId} locally, but ingest bootstrap failed: ${details}` +} + +async function ingestBootstrap(sessionId: string) { + const { KiloSessions } = await import("../../kilo-sessions/kilo-sessions") + return KiloSessions.bootstrap(sessionId) +} + +export async function bootstrapImportedSessionIngest( + sessionId: string, + input?: { + bootstrap?: (sessionId: string) => Promise + warn?: (message: string) => void + }, +) { + const run = input?.bootstrap ?? ingestBootstrap + const warn = + input?.warn ?? + ((message: string) => { + process.stderr.write(message) + process.stderr.write(EOL) + }) + + log.info("ingest bootstrap started", { sessionId }) + await run(sessionId) + .then(() => { + log.info("ingest bootstrap completed", { sessionId }) + }) + .catch((error) => { + log.error("ingest bootstrap failed", { sessionId, error }) + warn(ingestBootstrapWarning(sessionId, error)) + }) +} +// kilocode_change end + export const ImportCommand = cmd({ command: "import ", describe: "import session data from JSON file or URL", @@ -134,7 +174,6 @@ export const ImportCommand = cmd({ Database.use((db) => { db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run() - Database.effect(() => Bus.publish(Session.Event.Created, { info: exportData.info })) }) for (const msg of exportData.messages) { @@ -167,6 +206,10 @@ export const ImportCommand = cmd({ } } + // kilocode_change start + await bootstrapImportedSessionIngest(exportData.info.id) + // kilocode_change end + process.stdout.write(`Imported session: ${exportData.info.id}`) process.stdout.write(EOL) }) diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 7a22b3e78ce..62288eba6f8 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -198,8 +198,25 @@ export namespace KiloSessions { } export async function create(sessionId: string) { + const result = await bootstrap(sessionId) + if (!result) return { id: "", ingestPath: "" } + + void fullSync(sessionId).catch((error) => log.error("share full sync failed", { sessionId, error })) + + return result + } + + export async function bootstrap(sessionId: string) { + if (ingestDisabled) { + log.info("session bootstrap skipped: ingest disabled", { sessionId }) + return + } + const client = await getClient() - if (!client) return { id: "", ingestPath: "" } + if (!client) { + log.info("session bootstrap skipped: no client", { sessionId }) + return + } log.info("creating session", { sessionId }) @@ -216,7 +233,7 @@ export namespace KiloSessions { await Storage.write(["session_share", sessionId], result) - void fullSync(sessionId).catch((error) => log.error("share full sync failed", { sessionId, error })) + log.info("session bootstrap completed", { sessionId }) return result } diff --git a/packages/opencode/src/kilocode/enhance-prompt.ts b/packages/opencode/src/kilocode/enhance-prompt.ts new file mode 100644 index 00000000000..72fd1a47d03 --- /dev/null +++ b/packages/opencode/src/kilocode/enhance-prompt.ts @@ -0,0 +1,60 @@ +// kilocode_change - new file +import { Provider } from "@/provider/provider" +import { LLM } from "@/session/llm" +import { Agent } from "@/agent/agent" +import { Log } from "@/util/log" + +const log = Log.create({ service: "enhance-prompt" }) + +export function clean(text: string) { + const stripped = text.replace(/^```\w*\n?|```$/g, "").trim() + return stripped.replace(/^(['"])([\s\S]*)\1$/, "$2").trim() +} + +export async function enhancePrompt(text: string): Promise { + log.info("enhancing", { length: text.length }) + + const defaultModel = await Provider.defaultModel() + const model = + (await Provider.getSmallModel(defaultModel.providerID)) ?? + (await Provider.getModel(defaultModel.providerID, defaultModel.modelID)) + + const agent: Agent.Info = { + name: "enhance-prompt", + mode: "primary", + hidden: true, + options: {}, + permission: [], + prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", + temperature: 0.7, + } + + const stream = await LLM.stream({ + agent, + user: { + id: "enhance-prompt", + sessionID: "enhance-prompt", + role: "user", + model: { + providerID: model.providerID, + modelID: model.id, + }, + time: { + created: Date.now(), + completed: Date.now(), + }, + } as any, + tools: {}, + model, + small: true, + messages: [{ role: "user" as const, content: text }], + abort: new AbortController().signal, + sessionID: "enhance-prompt", + system: [], + retries: 3, + }) + + const result = await stream.text + log.info("enhanced", { length: result.length }) + return clean(result) +} diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index a30c670a43c..862eba4a07d 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -252,25 +252,39 @@ export namespace ProviderTransform { } // kilocode_change - function added - function fixDuplicateReasoning(msgs: ModelMessage[]) { + function fixDuplicateReasoning(msgs: ModelMessage[], model: Provider.Model) { for (const msg of msgs) { - if (!Array.isArray(msg.content)) { + if (msg.role !== "assistant" || !Array.isArray(msg.content)) { continue } - let isFirstToolCall = true + const set = new Set() for (const part of msg.content) { - if (part.type === "reasoning") { - // this entry is corrupt - delete part.providerOptions?.openrouter?.reasoning_details - } - if (part.type === "tool-call" && isFirstToolCall) { - isFirstToolCall = false + const openrouterProviderOptions = part.providerOptions?.openrouter as + | { + reasoning_details?: { data?: string; text?: string; signature?: string }[] + } + | undefined + if (!openrouterProviderOptions || !openrouterProviderOptions.reasoning_details) { continue } - if (part.type === "tool-call") { - // this is a duplicate entry - delete part.providerOptions?.openrouter?.reasoning_details - } + openrouterProviderOptions.reasoning_details = openrouterProviderOptions.reasoning_details.filter((rd) => { + if (rd.data) { + if (!set.has(rd.data)) { + set.add(rd.data) + return true + } + return false + } + if (rd.text) { + if ((model.family === "claude" || model.id.includes("claude")) && !rd.signature) return false + if (!set.has(rd.text)) { + set.add(rd.text) + return true + } + return false + } + return true + }) } } } @@ -282,7 +296,7 @@ export namespace ProviderTransform { // kilocode_change - workaround for @openrouter/ai-sdk-provider v1 duplicating reasoning // fixed in https://github.com/OpenRouterTeam/ai-sdk-provider/pull/344/ if (model.api.npm === "@kilocode/kilo-gateway") { - fixDuplicateReasoning(msgs) + fixDuplicateReasoning(msgs, model) } if ( diff --git a/packages/opencode/src/server/routes/enhance-prompt.ts b/packages/opencode/src/server/routes/enhance-prompt.ts new file mode 100644 index 00000000000..8fd49e1ece0 --- /dev/null +++ b/packages/opencode/src/server/routes/enhance-prompt.ts @@ -0,0 +1,40 @@ +// kilocode_change - new file +import { Hono } from "hono" +import { describeRoute, resolver, validator } from "hono-openapi" +import z from "zod" +import { enhancePrompt } from "../../kilocode/enhance-prompt" +import { lazy } from "../../util/lazy" +import { errors } from "../error" + +export const EnhancePromptRoutes = lazy(() => + new Hono().post( + "/", + describeRoute({ + summary: "Enhance prompt", + description: "Rewrite a user's draft prompt into a clearer, more specific, and more effective prompt.", + operationId: "enhancePrompt.enhance", + responses: { + 200: { + description: "Enhanced prompt text", + content: { + "application/json": { + schema: resolver(z.object({ text: z.string() })), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + text: z.string().min(1).meta({ description: "The user's draft prompt to enhance" }), + }), + ), + async (c) => { + const body = c.req.valid("json") + const result = await enhancePrompt(body.text) + return c.json({ text: result }) + }, + ), +) diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts index bd9f2294a79..da7fe3c8bfe 100644 --- a/packages/opencode/src/server/routes/experimental.ts +++ b/packages/opencode/src/server/routes/experimental.ts @@ -209,9 +209,18 @@ export const ExperimentalRoutes = lazy(() => ...errors(400), }, }), + // kilocode_change start + validator( + "query", + z.object({ + base: z.string().optional().meta({ description: "Base branch or ref to diff against" }), + }), + ), async (c) => { const log = Log.create({ service: "worktree-diff" }) - const base = c.req.query("base") || (await Review.getBaseBranch()) + const query = c.req.valid("query") + const base = query.base || (await Review.getBaseBranch()) + // kilocode_change end const dir = Instance.directory log.info("computing diff", { dir, base }) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c6a374a5461..c882207a8ee 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -43,6 +43,7 @@ import { websocket } from "hono/bun" import { HTTPException } from "hono/http-exception" import { errors } from "./error" import { CommitMessageRoutes } from "./routes/commit-message" +import { EnhancePromptRoutes } from "./routes/enhance-prompt" // kilocode_change import { QuestionRoutes } from "./routes/question" import { PermissionRoutes } from "./routes/permission" import { GlobalRoutes } from "./routes/global" @@ -241,6 +242,7 @@ export namespace Server { .route("/provider", ProviderRoutes()) .route("/telemetry", TelemetryRoutes()) // kilocode_change .route("/commit-message", CommitMessageRoutes()) // kilocode_change + .route("/enhance-prompt", EnhancePromptRoutes()) // kilocode_change // kilocode_change start - Kilo Gateway routes .route( "/kilo", diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b3dbde6f37c..17606be6ff1 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -768,8 +768,7 @@ export namespace SessionPrompt { SessionCompaction.prune({ sessionID }) // kilocode_change start finished = true - abort.throwIfAborted() - // kilocode_change end + // Return the stored interrupted assistant turn before surfacing AbortError. for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user") continue const queued = state()[sessionID]?.callbacks ?? [] @@ -778,6 +777,8 @@ export namespace SessionPrompt { } return item } + if (abort.aborted) abort.throwIfAborted() + // kilocode_change end throw new Error("Impossible") }) diff --git a/packages/opencode/test/cli/import.test.ts b/packages/opencode/test/cli/import.test.ts index a1a69dc0941..e32de1342e4 100644 --- a/packages/opencode/test/cli/import.test.ts +++ b/packages/opencode/test/cli/import.test.ts @@ -1,5 +1,11 @@ import { test, expect } from "bun:test" -import { parseShareUrl, transformShareData, type ShareData } from "../../src/cli/cmd/import" +import { + parseShareUrl, + transformShareData, + bootstrapImportedSessionIngest, + ingestBootstrapWarning, + type ShareData, +} from "../../src/cli/cmd/import" // parseShareUrl tests test("parses valid share URLs", () => { @@ -36,3 +42,41 @@ test("returns null for invalid share data", () => { expect(transformShareData([{ type: "message", data: {} as any }])).toBeNull() expect(transformShareData([{ type: "session", data: { id: "s" } as any }])).toBeNull() // no messages }) + +test("formats ingest bootstrap warning", () => { + expect(ingestBootstrapWarning("session-123", new Error("network failed"))).toContain("session-123") + expect(ingestBootstrapWarning("session-123", new Error("network failed"))).toContain("network failed") + expect(ingestBootstrapWarning("session-123", "oops")).toContain("oops") +}) + +test("bootstrapImportedSessionIngest runs bootstrap and does not warn on success", async () => { + const calls: string[] = [] + const warnings: string[] = [] + + await bootstrapImportedSessionIngest("session-success", { + bootstrap: async (sessionId) => { + calls.push(sessionId) + }, + warn: (message) => warnings.push(message), + }) + + expect(calls).toEqual(["session-success"]) + expect(warnings).toHaveLength(0) +}) + +test("bootstrapImportedSessionIngest warns and continues on failure", async () => { + const warnings: string[] = [] + + await expect( + bootstrapImportedSessionIngest("session-fail", { + bootstrap: async () => { + throw new Error("boom") + }, + warn: (message) => warnings.push(message), + }), + ).resolves.toBeUndefined() + + expect(warnings).toHaveLength(1) + expect(warnings[0]).toContain("session-fail") + expect(warnings[0]).toContain("boom") +}) diff --git a/packages/opencode/test/kilocode/enhance-prompt.test.ts b/packages/opencode/test/kilocode/enhance-prompt.test.ts new file mode 100644 index 00000000000..2bd0422ffd1 --- /dev/null +++ b/packages/opencode/test/kilocode/enhance-prompt.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from "bun:test" +import { clean } from "../../src/kilocode/enhance-prompt" + +describe("enhance-prompt", () => { + describe("clean", () => { + it("trims whitespace", () => { + expect(clean(" hello world ")).toBe("hello world") + }) + + it("strips code block markers", () => { + expect(clean("```\nhello world\n```")).toBe("hello world") + }) + + it("strips code block with language tag", () => { + expect(clean("```text\nhello world\n```")).toBe("hello world") + }) + + it("strips surrounding double quotes", () => { + expect(clean('"hello world"')).toBe("hello world") + }) + + it("strips surrounding single quotes", () => { + expect(clean("'hello world'")).toBe("hello world") + }) + + it("strips code blocks and quotes together", () => { + expect(clean('```\n"hello world"\n```')).toBe("hello world") + }) + + it("returns plain text unchanged", () => { + expect(clean("hello world")).toBe("hello world") + }) + + it("handles empty string", () => { + expect(clean("")).toBe("") + }) + + it("handles whitespace-only string", () => { + expect(clean(" ")).toBe("") + }) + + it("does not strip internal quotes", () => { + expect(clean('say "hello" to the world')).toBe('say "hello" to the world') + }) + + it("does not strip mismatched quotes", () => { + expect(clean("\"hello world'")).toBe("\"hello world'") + }) + }) + + +}) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index e8a8c65b03d..9e0ac582e7c 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1,4 +1,5 @@ import path from "path" +import fs from "fs/promises" import { describe, expect, test } from "bun:test" import { fileURLToPath } from "url" import { Instance } from "../../src/project/instance" @@ -209,3 +210,123 @@ describe("session.prompt agent variant", () => { } }) }) + +// kilocode_change start +function deferred() { + const result = {} as { promise: Promise; resolve: (value: T) => void } + result.promise = new Promise((resolve) => { + result.resolve = resolve + }) + return result +} + +describe("session.prompt abort", () => { + test("returns the interrupted assistant turn when the current prompt is cancelled", async () => { + const dir = path.dirname(fileURLToPath(import.meta.url)) + const fixtures = (await Bun.file(path.join(dir, "../tool/fixtures/models-api.json")).json()) as Record< + string, + { models: Record } & Record + > + const model = fixtures.openai.models["gpt-5.2"] + const started = deferred() + const payload = new TextEncoder().encode( + [ + `data: ${JSON.stringify({ + type: "response.created", + response: { + id: "resp-1", + created_at: Math.floor(Date.now() / 1000), + model: model.id, + service_tier: null, + }, + })}`, + "", + ].join("\n\n"), + ) + const server = Bun.serve({ + port: 0, + fetch(req: Request) { + const url = new URL(req.url) + if (!url.pathname.endsWith("/responses")) { + return new Response("unexpected request", { status: 404 }) + } + started.resolve() + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(payload) + }, + }), + { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }, + ) + }, + }) + + try { + await using tmp = await tmpdir({ + git: true, + init: async (root) => { + const dir = path.join(root, ".opencode") + await fs.mkdir(dir, { recursive: true }) + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://app.kilo.ai/config.json", + enabled_providers: ["openai"], + provider: { + openai: { + name: "OpenAI", + env: ["OPENAI_API_KEY"], + npm: "@ai-sdk/openai", + api: "https://api.openai.com/v1", + models: { + [model.id]: model, + }, + options: { + apiKey: "test-openai-key", + baseURL: `${server.url.origin}/v1`, + }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const run = SessionPrompt.prompt({ + sessionID: session.id, + model: { + providerID: "openai", + modelID: model.id, + }, + parts: [{ type: "text", text: "say hello" }], + }) + + await started.promise + SessionPrompt.cancel(session.id) + + const result = await run + expect(result.info.role).toBe("assistant") + if (result.info.role !== "assistant") throw new Error("expected assistant message") + expect(result.info.error?.name).toBe("MessageAbortedError") + + const messages = await Session.messages({ sessionID: session.id }) + const assistant = messages.find((item) => item.info.role === "assistant") + expect(assistant?.info.id).toBe(result.info.id) + + await Session.remove(session.id) + }, + }) + } finally { + server.stop(true) + } + }, 15000) +}) +// kilocode_change end diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 7aaff4c43c7..b6b6b3d21bf 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/plugin", - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "repository": { diff --git a/packages/script/package.json b/packages/script/package.json index a0600588c77..4c16ba1ea98 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -8,7 +8,7 @@ "exports": { ".": "./src/index.ts" }, - "version": "7.0.33", + "version": "7.0.35", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f8eae8592e8..8abcc4902ef 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/sdk", - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "repository": { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 7aaaca41ff9..076d4e208a1 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -21,6 +21,8 @@ import type { ConfigProvidersResponses, ConfigUpdateErrors, ConfigUpdateResponses, + EnhancePromptEnhanceErrors, + EnhancePromptEnhanceResponses, EventSubscribeResponses, EventTuiCommandExecute, EventTuiPromptAppend, @@ -928,10 +930,21 @@ export class Worktree extends HeyApiClient { public diff( parameters?: { directory?: string + base?: string }, options?: Options, ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "base" }, + ], + }, + ], + ) return (options?.client ?? this.client).get({ url: "/experimental/worktree/diff", ...options, @@ -2395,6 +2408,47 @@ export class CommitMessage extends HeyApiClient { } } +export class EnhancePrompt extends HeyApiClient { + /** + * Enhance prompt + * + * Rewrite a user's draft prompt into a clearer, more specific, and more effective prompt. + */ + public enhance( + parameters?: { + directory?: string + text?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "body", key: "text" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + EnhancePromptEnhanceResponses, + EnhancePromptEnhanceErrors, + ThrowOnError + >({ + url: "/enhance-prompt", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Organization extends HeyApiClient { /** * Update Kilo Gateway organization @@ -3739,6 +3793,11 @@ export class KiloClient extends HeyApiClient { return (this._commitMessage ??= new CommitMessage({ client: this.client })) } + private _enhancePrompt?: EnhancePrompt + get enhancePrompt(): EnhancePrompt { + return (this._enhancePrompt ??= new EnhancePrompt({ client: this.client })) + } + private _kilo?: Kilo get kilo(): Kilo { return (this._kilo ??= new Kilo({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 03b02cd9efc..47b9b58eff4 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2540,6 +2540,10 @@ export type WorktreeDiffData = { path?: never query?: { directory?: string + /** + * Base branch or ref to diff against + */ + base?: string } url: "/experimental/worktree/diff" } @@ -4067,6 +4071,40 @@ export type CommitMessageGenerateResponses = { export type CommitMessageGenerateResponse = CommitMessageGenerateResponses[keyof CommitMessageGenerateResponses] +export type EnhancePromptEnhanceData = { + body?: { + /** + * The user's draft prompt to enhance + */ + text: string + } + path?: never + query?: { + directory?: string + } + url: "/enhance-prompt" +} + +export type EnhancePromptEnhanceErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type EnhancePromptEnhanceError = EnhancePromptEnhanceErrors[keyof EnhancePromptEnhanceErrors] + +export type EnhancePromptEnhanceResponses = { + /** + * Enhanced prompt text + */ + 200: { + text: string + } +} + +export type EnhancePromptEnhanceResponse = EnhancePromptEnhanceResponses[keyof EnhancePromptEnhanceResponses] + export type KiloProfileData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 114034fc2aa..2281da94483 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -36,7 +36,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.global.health({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.global.health({\n ...\n})" } ] } @@ -61,7 +61,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.global.event({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.global.event({\n ...\n})" } ] } @@ -86,7 +86,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.get({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.global.config.get({\n ...\n})" } ] }, @@ -128,7 +128,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.global.config.update({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.global.config.update({\n ...\n})" } ] } @@ -153,7 +153,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.global.dispose({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.global.dispose({\n ...\n})" } ] } @@ -207,7 +207,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.auth.set({\n ...\n})" } ] }, @@ -250,7 +250,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.auth.remove({\n ...\n})" } ] } @@ -287,7 +287,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.project.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.project.list({\n ...\n})" } ] } @@ -321,7 +321,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.project.current({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.project.current({\n ...\n})" } ] } @@ -420,7 +420,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.project.update({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.project.update({\n ...\n})" } ] } @@ -457,7 +457,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.pty.list({\n ...\n})" } ] }, @@ -534,7 +534,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.create({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.pty.create({\n ...\n})" } ] } @@ -586,7 +586,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.get({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.pty.get({\n ...\n})" } ] }, @@ -662,7 +662,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.update({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.pty.update({\n ...\n})" } ] }, @@ -712,7 +712,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.remove({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.pty.remove({\n ...\n})" } ] } @@ -764,7 +764,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.pty.connect({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.pty.connect({\n ...\n})" } ] } @@ -798,7 +798,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.config.get({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.config.get({\n ...\n})" } ] }, @@ -849,7 +849,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.config.update({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.config.update({\n ...\n})" } ] } @@ -901,7 +901,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.config.providers({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.config.providers({\n ...\n})" } ] } @@ -945,7 +945,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.ids({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tool.ids({\n ...\n})" } ] } @@ -1005,7 +1005,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tool.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tool.list({\n ...\n})" } ] } @@ -1058,7 +1058,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.create({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.worktree.create({\n ...\n})" } ] }, @@ -1093,7 +1093,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.worktree.list({\n ...\n})" } ] }, @@ -1144,7 +1144,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.remove({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.worktree.remove({\n ...\n})" } ] } @@ -1197,7 +1197,62 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.worktree.reset({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.worktree.reset({\n ...\n})" + } + ] + } + }, + "/experimental/worktree/diff": { + "get": { + "operationId": "worktree.diff", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "base", + "schema": { + "type": "string" + }, + "description": "Base branch or ref to diff against" + } + ], + "summary": "Get worktree diff", + "description": "Get file diffs for a worktree compared to its base branch. Includes uncommitted changes.", + "responses": { + "200": { + "description": "File diffs", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.worktree.diff({\n ...\n})" } ] } @@ -1283,7 +1338,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.session.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.session.list({\n ...\n})" } ] } @@ -1323,7 +1378,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.resource.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.experimental.resource.list({\n ...\n})" } ] } @@ -1393,7 +1448,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.list({\n ...\n})" } ] }, @@ -1459,7 +1514,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.create({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.create({\n ...\n})" } ] } @@ -1509,7 +1564,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.status({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.status({\n ...\n})" } ] } @@ -1573,7 +1628,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.get({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.get({\n ...\n})" } ] }, @@ -1634,7 +1689,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.delete({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.delete({\n ...\n})" } ] }, @@ -1716,7 +1771,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.update({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.update({\n ...\n})" } ] } @@ -1783,7 +1838,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.children({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.children({\n ...\n})" } ] } @@ -1849,7 +1904,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.todo({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.todo({\n ...\n})" } ] } @@ -1934,7 +1989,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.init({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.init({\n ...\n})" } ] } @@ -1992,7 +2047,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.fork({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.fork({\n ...\n})" } ] } @@ -2054,7 +2109,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.abort({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.abort({\n ...\n})" } ] } @@ -2116,7 +2171,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.share({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.share({\n ...\n})" } ] }, @@ -2177,7 +2232,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unshare({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.unshare({\n ...\n})" } ] } @@ -2231,7 +2286,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.diff({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.diff({\n ...\n})" } ] } @@ -2316,7 +2371,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.summarize({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.summarize({\n ...\n})" } ] } @@ -2401,7 +2456,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.messages({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.messages({\n ...\n})" } ] }, @@ -2572,7 +2627,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.prompt({\n ...\n})" } ] } @@ -2656,7 +2711,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.message({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.message({\n ...\n})" } ] }, @@ -2726,7 +2781,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.deleteMessage({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.deleteMessage({\n ...\n})" } ] } @@ -2806,7 +2861,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.part.delete({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.part.delete({\n ...\n})" } ] }, @@ -2893,7 +2948,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.part.update({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.part.update({\n ...\n})" } ] } @@ -3047,7 +3102,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.prompt_async({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.prompt_async({\n ...\n})" } ] } @@ -3185,7 +3240,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.command({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.command({\n ...\n})" } ] } @@ -3278,7 +3333,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.shell({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.shell({\n ...\n})" } ] } @@ -3360,7 +3415,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.revert({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.revert({\n ...\n})" } ] } @@ -3422,7 +3477,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.session.unrevert({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.session.unrevert({\n ...\n})" } ] } @@ -3509,7 +3564,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.respond({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.permission.respond({\n ...\n})" } ] } @@ -3590,7 +3645,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.reply({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.permission.reply({\n ...\n})" } ] } @@ -3627,7 +3682,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.permission.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.permission.list({\n ...\n})" } ] } @@ -3664,7 +3719,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.question.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.question.list({\n ...\n})" } ] } @@ -3745,7 +3800,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reply({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.question.reply({\n ...\n})" } ] } @@ -3807,7 +3862,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.question.reject({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.question.reject({\n ...\n})" } ] } @@ -3979,12 +4034,13 @@ }, "required": ["input", "output"] }, - "recommended": { - "type": "boolean" - }, "recommendedIndex": { "type": "number" }, + "prompt": { + "type": "string", + "enum": ["codex", "gemini", "beast", "anthropic", "trinity", "anthropic_without_todo"] + }, "experimental": { "type": "boolean" }, @@ -4075,7 +4131,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.provider.list({\n ...\n})" } ] } @@ -4118,7 +4174,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.auth({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.provider.auth({\n ...\n})" } ] } @@ -4187,7 +4243,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.authorize({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.provider.oauth.authorize({\n ...\n})" } ] } @@ -4260,7 +4316,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.provider.oauth.callback({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.provider.oauth.callback({\n ...\n})" } ] } @@ -4328,7 +4384,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.telemetry.capture({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.telemetry.capture({\n ...\n})" } ] } @@ -4405,7 +4461,74 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.commitMessage.generate({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.commitMessage.generate({\n ...\n})" + } + ] + } + }, + "/enhance-prompt": { + "post": { + "operationId": "enhancePrompt.enhance", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Enhance prompt", + "description": "Rewrite a user's draft prompt into a clearer, more specific, and more effective prompt.", + "responses": { + "200": { + "description": "Enhanced prompt text", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": { + "description": "The user's draft prompt to enhance", + "type": "string", + "minLength": 1 + } + }, + "required": ["text"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.enhancePrompt.enhance({\n ...\n})" } ] } @@ -4508,7 +4631,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.profile({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilo.profile({\n ...\n})" } ] } @@ -4574,7 +4697,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.organization.set({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilo.organization.set({\n ...\n})" } ] } @@ -4598,7 +4721,41 @@ "description": "Streaming FIM completion response", "content": { "text/event-stream": { - "schema": {} + "schema": { + "type": "object", + "properties": { + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "delta": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + } + } + } + }, + "usage": { + "type": "object", + "properties": { + "prompt_tokens": { + "type": "number" + }, + "completion_tokens": { + "type": "number" + } + } + }, + "cost": { + "type": "number" + } + } + } } } }, @@ -4643,7 +4800,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.fim({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilo.fim({\n ...\n})" } ] } @@ -4720,7 +4877,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.notifications({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilo.notifications({\n ...\n})" } ] } @@ -4770,7 +4927,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.cloud.session.get({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilo.cloud.session.get({\n ...\n})" } ] } @@ -4837,7 +4994,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.cloud.session.import({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilo.cloud.session.import({\n ...\n})" } ] } @@ -4946,7 +5103,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.kilo.cloudSessions({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.kilo.cloudSessions({\n ...\n})" } ] } @@ -5042,7 +5199,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.find.text({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.find.text({\n ...\n})" } ] } @@ -5112,7 +5269,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.find.files({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.find.files({\n ...\n})" } ] } @@ -5157,7 +5314,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.find.symbols({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.find.symbols({\n ...\n})" } ] } @@ -5202,7 +5359,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.file.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.file.list({\n ...\n})" } ] } @@ -5244,7 +5401,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.file.read({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.file.read({\n ...\n})" } ] } @@ -5281,7 +5438,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.file.status({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.file.status({\n ...\n})" } ] } @@ -5321,7 +5478,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.status({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.status({\n ...\n})" } ] }, @@ -5394,7 +5551,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.add({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.add({\n ...\n})" } ] } @@ -5463,7 +5620,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.start({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.auth.start({\n ...\n})" } ] }, @@ -5520,7 +5677,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.remove({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.auth.remove({\n ...\n})" } ] } @@ -5598,7 +5755,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.callback({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.auth.callback({\n ...\n})" } ] } @@ -5660,7 +5817,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.auth.authenticate({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.auth.authenticate({\n ...\n})" } ] } @@ -5701,7 +5858,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.connect({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.connect({\n ...\n})" } ] } @@ -5742,7 +5899,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.mcp.disconnect({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.mcp.disconnect({\n ...\n})" } ] } @@ -5801,7 +5958,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.appendPrompt({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.appendPrompt({\n ...\n})" } ] } @@ -5835,7 +5992,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openHelp({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.openHelp({\n ...\n})" } ] } @@ -5869,7 +6026,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openSessions({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.openSessions({\n ...\n})" } ] } @@ -5903,7 +6060,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openThemes({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.openThemes({\n ...\n})" } ] } @@ -5937,7 +6094,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.openModels({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.openModels({\n ...\n})" } ] } @@ -5971,7 +6128,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.submitPrompt({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.submitPrompt({\n ...\n})" } ] } @@ -6005,7 +6162,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.clearPrompt({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.clearPrompt({\n ...\n})" } ] } @@ -6064,7 +6221,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.executeCommand({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.executeCommand({\n ...\n})" } ] } @@ -6125,7 +6282,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.showToast({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.showToast({\n ...\n})" } ] } @@ -6191,7 +6348,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.publish({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.publish({\n ...\n})" } ] } @@ -6262,7 +6419,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.selectSession({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.selectSession({\n ...\n})" } ] } @@ -6303,7 +6460,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.next({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.control.next({\n ...\n})" } ] } @@ -6344,7 +6501,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.tui.control.response({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.tui.control.response({\n ...\n})" } ] } @@ -6378,7 +6535,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.instance.dispose({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.instance.dispose({\n ...\n})" } ] } @@ -6412,7 +6569,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.path.get({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.path.get({\n ...\n})" } ] } @@ -6446,7 +6603,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.get({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.vcs.get({\n ...\n})" } ] } @@ -6483,7 +6640,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.command.list({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.command.list({\n ...\n})" } ] } @@ -6560,7 +6717,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.app.log({\n ...\n})" } ] } @@ -6597,7 +6754,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.app.agents({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.app.agents({\n ...\n})" } ] } @@ -6649,7 +6806,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.app.skills({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.app.skills({\n ...\n})" } ] } @@ -6686,7 +6843,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.lsp.status({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.lsp.status({\n ...\n})" } ] } @@ -6723,7 +6880,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.formatter.status({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.formatter.status({\n ...\n})" } ] } @@ -6757,7 +6914,7 @@ "x-codeSamples": [ { "lang": "js", - "source": "import { createOpencodeClient } from \"@kilocode/sdk\n\nconst client = createOpencodeClient()\nawait client.event.subscribe({\n ...\n})" + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.event.subscribe({\n ...\n})" } ] } @@ -9903,12 +10060,13 @@ }, "required": ["input", "output"] }, - "recommended": { - "type": "boolean" - }, "recommendedIndex": { "type": "number" }, + "prompt": { + "type": "string", + "enum": ["codex", "gemini", "beast", "anthropic", "trinity", "anthropic_without_todo"] + }, "experimental": { "type": "boolean" }, @@ -10859,11 +11017,12 @@ "additionalProperties": {} } }, - "recommended": { - "type": "boolean" - }, "recommendedIndex": { "type": "number" + }, + "prompt": { + "type": "string", + "enum": ["codex", "gemini", "beast", "anthropic", "trinity", "anthropic_without_todo"] } }, "required": [ diff --git a/packages/ui/package.json b/packages/ui/package.json index 8107fa4e6c0..137d8682c34 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.0.33", + "version": "7.0.35", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 31d79e01838..5c89bc1d4e8 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "7.0.33", + "version": "7.0.35", "private": true, "type": "module", "license": "MIT", diff --git a/script/upstream/package.json b/script/upstream/package.json index 152b6f1e332..e964a7cee38 100644 --- a/script/upstream/package.json +++ b/script/upstream/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/upstream-merge", - "version": "7.0.33", + "version": "7.0.35", "private": true, "type": "module", "description": "Scripts for automating upstream opencode merges into Kilo", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index b2b6130fe20..6bb71c245bc 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "7.0.33", + "version": "7.0.35", "publisher": "sst-dev", "repository": { "type": "git",