diff --git a/.changeset/bright-badgers-bring-keys.md b/.changeset/bright-badgers-bring-keys.md new file mode 100644 index 0000000000..f7dd403a71 --- /dev/null +++ b/.changeset/bright-badgers-bring-keys.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": minor +"@kilocode/kilo-gateway": minor +"kilo-code": minor +--- + +Show a BYOK badge for Kilo Gateway models that can use an enabled personal or organization provider key. diff --git a/.changeset/deny-gateway-data-collection.md b/.changeset/deny-gateway-data-collection.md new file mode 100644 index 0000000000..48286252fa --- /dev/null +++ b/.changeset/deny-gateway-data-collection.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-gateway": patch +--- + +Deny provider data collection for Kilo Gateway requests when prompt-training models are hidden. diff --git a/.changeset/hide-reverted-provider-errors.md b/.changeset/hide-reverted-provider-errors.md new file mode 100644 index 0000000000..c54d4e302b --- /dev/null +++ b/.changeset/hide-reverted-provider-errors.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Hide reverted provider errors so Redo controls remain visible after rewinding a session. diff --git a/.changeset/keep-diff-identifiers-intact.md b/.changeset/keep-diff-identifiers-intact.md new file mode 100644 index 0000000000..57c03dbff5 --- /dev/null +++ b/.changeset/keep-diff-identifiers-intact.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep changed identifiers intact when highlighting edits within diff lines. diff --git a/.changeset/quiet-subagent-finishes.md b/.changeset/quiet-subagent-finishes.md new file mode 100644 index 0000000000..0c0cc679ff --- /dev/null +++ b/.changeset/quiet-subagent-finishes.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": patch +"@kilocode/sdk": patch +"kilo-code": patch +--- + +Limit completion sounds to parent agent sessions. diff --git a/.changeset/quiet-terminal-panel.md b/.changeset/quiet-terminal-panel.md new file mode 100644 index 0000000000..794b02b799 --- /dev/null +++ b/.changeset/quiet-terminal-panel.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Avoid failing Agent Manager startup when another extension already registered VS Code panel commands. diff --git a/.changeset/roo-code-session-import.md b/.changeset/roo-code-session-import.md deleted file mode 100644 index 4bbdc50e57..0000000000 --- a/.changeset/roo-code-session-import.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Add an "Import Sessions from Roo Code" button in the About settings tab that discovers and imports conversation history from an existing Roo Code installation, reusing the existing migration wizard. diff --git a/.changeset/stabilize-agent-manager-transcripts.md b/.changeset/stabilize-agent-manager-transcripts.md new file mode 100644 index 0000000000..be190dce0c --- /dev/null +++ b/.changeset/stabilize-agent-manager-transcripts.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Prevent concurrent subagent updates from blanking the Agent Manager webview. diff --git a/.changeset/swift-pr-hover-cards.md b/.changeset/swift-pr-hover-cards.md new file mode 100644 index 0000000000..3b154302b2 --- /dev/null +++ b/.changeset/swift-pr-hover-cards.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Speed up Agent Manager worktree hover cards so pull request details appear and dismiss more quickly. diff --git a/.changeset/widen-chat-lane-further.md b/.changeset/widen-chat-lane-further.md new file mode 100644 index 0000000000..8e74036f20 --- /dev/null +++ b/.changeset/widen-chat-lane-further.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Widen the chat readable lane from 88ch to 98ch so conversations, tools, and diffs can use more editor space. diff --git a/.github/workflows/watch-opencode-releases.yml b/.github/workflows/watch-opencode-releases.yml index 10da78231b..a3d766b7ae 100644 --- a/.github/workflows/watch-opencode-releases.yml +++ b/.github/workflows/watch-opencode-releases.yml @@ -40,9 +40,12 @@ jobs: env: URL: ${{ secrets.OPENCODE_WATCH_SYNC_URL }} SECRET: ${{ secrets.OPENCODE_WATCH_SYNC_SECRET }} + VERCEL: ${{ secrets.OPENCODE_WATCH_VERCEL_SECRET }} run: | if [ -n "$URL" ] && [ -n "$SECRET" ]; then - curl -sf -X POST "$URL" -H "x-sync-secret: $SECRET" -H "Content-Type: application/json" -d '{}' || true + headers=(-H "x-sync-secret: $SECRET" -H "Content-Type: application/json") + [ -n "$VERCEL" ] && headers+=(-H "x-vercel-protection-bypass: $VERCEL") + curl -sf -X POST "$URL" "${headers[@]}" -d '{}' || true fi - if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true' diff --git a/bun.lock b/bun.lock index b16cd5d81b..78bafbc5e6 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.3.48", + "version": "7.3.49", "bin": { "opencode": "./bin/opencode", }, @@ -90,7 +90,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -104,7 +104,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@effect/platform-node": "catalog:", "effect": "catalog:", @@ -117,7 +117,7 @@ }, "packages/kilo-console": { "name": "@kilocode/kilo-console", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", @@ -139,7 +139,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -169,7 +169,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -205,7 +205,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.3.48", + "version": "7.3.49", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -215,7 +215,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -247,11 +247,11 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.3.48", + "version": "7.3.49", }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -265,7 +265,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -302,7 +302,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", @@ -354,6 +354,7 @@ "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", + "happy-dom": "20.8.9", "knip": "5.85.0", "prettier": "3.6.2", "qrcode": "^1.5.4", @@ -368,7 +369,7 @@ }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -385,7 +386,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -403,7 +404,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.3.48", + "version": "7.3.49", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -550,7 +551,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -578,7 +579,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -592,7 +593,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "semver": "^7.6.3", }, @@ -603,7 +604,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "cross-spawn": "catalog:", }, @@ -618,7 +619,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.3.48", + "version": "7.3.49", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -641,7 +642,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -2204,6 +2205,8 @@ "@types/vscode": ["@types/vscode@1.120.0", "", {}, "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -2486,6 +2489,8 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -3076,6 +3081,8 @@ "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "happy-dom": ["happy-dom@20.10.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -4270,7 +4277,7 @@ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -4630,6 +4637,8 @@ "cheerio/undici": ["undici@7.27.2", "", {}, "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA=="], + "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], diff --git a/nix/hashes.json b/nix/hashes.json index fdee193a50..575425ce92 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-ViNoPxLiExkP4OC7F0AfXnE2Ib+oi4qtP0hqnfhV2Ho=", - "aarch64-linux": "sha256-TKp6yzSijtElRhtcInC6b9LOjvUrLunyRUXBwOb/4i8=", - "aarch64-darwin": "sha256-kW9kVAHOMpKF0/d/YOZFIENI5KyHGLwDvijd/JprYHg=", - "x86_64-darwin": "sha256-8m1uCjLAQHoEVoBETPzVVV5ji8Zlllf1f9s+d8HmLpY=" + "x86_64-linux": "sha256-6wXsrAKFGbHf3bfRmbKMXsrI4jYOzzOkwcyFnM4JVK4=", + "aarch64-linux": "sha256-Vmkvv4HEf0DQA+B2khHfAvGRjxiawtQjUeN+G+SNbH0=", + "aarch64-darwin": "sha256-u5mIwNkEmf2sJBHH/Lm3o+OXtDbl7rLG38bkLRUH1cY=", + "x86_64-darwin": "sha256-MuA/788i7hYZurDjJJnFUr/BHF9sP/mg0BvxCHQsbCY=" } } diff --git a/package.json b/package.json index cdc98292b4..a45edfc06f 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,6 @@ "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" }, - "version": "7.3.48", + "version": "7.3.49", "peerDependencies": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 8c78fc142b..6a9be3c502 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index b7628cd6b1..7c1a5ef67b 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index e105884ba4..2def83eb98 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.3.48" +version = "7.3.49" 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.3.48/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/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.3.48/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/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.3.48/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/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.3.48/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 58f9e5215b..196d5d7e47 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/http-recorder", "type": "module", "license": "MIT", diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json index 2a98358b41..6903d0e35c 100755 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-console", - "version": "7.3.48", + "version": "7.3.49", "private": true, "type": "module", "scripts": { diff --git a/packages/kilo-console/src/client.ts b/packages/kilo-console/src/client.ts index 076b5f4a88..b1e74cadc1 100644 --- a/packages/kilo-console/src/client.ts +++ b/packages/kilo-console/src/client.ts @@ -11,6 +11,7 @@ import type { GlobalHealthResponse, GlobalEvent, KiloEmbeddingModelCatalog, + KiloProfileResponse, LspStatusResponse, McpStatusResponse, Pty as PtyInfo, @@ -42,6 +43,8 @@ export type Query = { export type ProjectQuery = Pick +export type KiloProfileData = KiloProfileResponse + export type ProjectItem = KiloProject export type RecentProjectItem = ProjectItem & { sessions: number @@ -210,6 +213,11 @@ function model(input: unknown) { return { provider: input.slice(0, index), model: input.slice(index + 1) } } +function directory(input: ProjectQuery) { + const dir = value(input.dir) + return dir ? { directory: dir } : undefined +} + function title(input: string) { return input .split("_") @@ -407,6 +415,42 @@ export async function loadEmbeddingModels(input: Query): Promise { + const sdk = client(input) + const result = await sdk.kilo.profile(directory(input)) + return demand("Kilo profile", result) +} + +export async function setKiloOrganization(input: ProjectQuery, organizationId: string | null) { + const sdk = client(input) + const result = await sdk.kilo.organization.set({ ...directory(input), organizationId }) + demand("Switch Kilo account", result) + await sdk.global.dispose() +} + +export async function logoutKilo(input: ProjectQuery) { + const sdk = client(input) + const result = await sdk.auth.remove({ providerID: "kilo" }) + demand("Log out of Kilo", result) + await sdk.global.dispose() +} + +export async function startKiloLogin(input: ProjectQuery): Promise { + const sdk = client(input) + const result = await sdk.provider.oauth.authorize({ ...directory(input), providerID: "kilo", method: 0 }) + return demand("Start Kilo login", result) +} + +export async function completeKiloLogin(input: ProjectQuery, signal?: AbortSignal) { + const sdk = client(input) + const result = await sdk.provider.oauth.callback( + { ...directory(input), providerID: "kilo", method: 0 }, + signal ? { signal } : undefined, + ) + demand("Complete Kilo login", result) + await sdk.global.dispose() +} + export async function loadProjects(input: ProjectQuery): Promise { const sdk = client(input) const dir = value(input.dir) diff --git a/packages/kilo-console/src/index.tsx b/packages/kilo-console/src/index.tsx index 786f9c9cad..13036c76ff 100644 --- a/packages/kilo-console/src/index.tsx +++ b/packages/kilo-console/src/index.tsx @@ -6,6 +6,7 @@ import "./styles.css" import { ProjectConsoleRoute } from "./routes/projects/ProjectConsoleRoute" import { ProjectsRoute } from "./routes/projects/ProjectsRoute" import { ProfileRoute } from "./routes/profile/ProfileRoute" +import { LoginRoute } from "./routes/profile/LoginRoute" import { ConfigLayout } from "./layouts/ConfigLayout" import { configSections } from "./routes/config/sections" @@ -27,6 +28,7 @@ render( {routes()} + {routes()} diff --git a/packages/kilo-console/src/routes/config/AgentsRoute.tsx b/packages/kilo-console/src/routes/config/AgentsRoute.tsx index 36d740df0d..25299e7b10 100644 --- a/packages/kilo-console/src/routes/config/AgentsRoute.tsx +++ b/packages/kilo-console/src/routes/config/AgentsRoute.tsx @@ -265,7 +265,10 @@ export function AgentBuilderRoute() { onInput={(event) => state.setDesc(event.currentTarget.value)} /> - + diff --git a/packages/kilo-console/src/routes/config/ModelsRoute.tsx b/packages/kilo-console/src/routes/config/ModelsRoute.tsx index d87e93e914..2d5ed1230b 100644 --- a/packages/kilo-console/src/routes/config/ModelsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ModelsRoute.tsx @@ -40,6 +40,7 @@ function chips(model: Model) { const list: string[] = [] if (model.capabilities.toolcall) list.push("toolcall") if (model.capabilities.attachment) list.push("attachment") + if (model.capabilities.temperature) list.push("temperature") if (model.capabilities.input.image) list.push("vision") if (model.capabilities.input.audio || model.capabilities.output.audio) list.push("audio") return list @@ -87,6 +88,7 @@ function Stat(props: { label: string; value: string; sub?: string; mono?: boolea const caps = [ { key: "toolcall", label: "toolcall" }, { key: "attachment", label: "attachment" }, + { key: "temperature", label: "temperature" }, { key: "input:image", label: "vision" }, { key: "input:audio", label: "audio" }, ] satisfies { key: Capability; label: string }[] @@ -476,6 +478,7 @@ export function ModelsAvailableRoute() { +
diff --git a/packages/kilo-console/src/routes/profile/LoginRoute.tsx b/packages/kilo-console/src/routes/profile/LoginRoute.tsx new file mode 100644 index 0000000000..3fde3f6567 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/LoginRoute.tsx @@ -0,0 +1,244 @@ +import { useLocation, useNavigate } from "@solidjs/router" +import { Button } from "@kilocode/kilo-web-ui/button" +import { Card } from "@kilocode/kilo-web-ui/card" +import { createEffect, createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" +import { LoadingScreen } from "../../components/LoadingScreen" +import { completeKiloLogin, loadKiloProfile, startKiloLogin, type ProjectQuery } from "../../client" +import { errMsg } from "../../shared/utils" +import { markDisconnected, page, parseDeviceCode, safeReturn } from "./profile-utils" +import { useProfileServer } from "./server" + +type Status = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled" + +type State = { + status: Status + code?: string + url?: string + expiresIn?: number + error?: string +} + +function time(input: number) { + const min = Math.floor(input / 60) + const sec = input % 60 + return `${min}:${sec.toString().padStart(2, "0")}` +} + +export function LoginRoute() { + const loc = useLocation() + const nav = useNavigate() + const params = createMemo(() => new URLSearchParams(loc.search)) + const server = useProfileServer(params) + const [auth, setAuth] = createSignal({ status: "idle" }) + const [left, setLeft] = createSignal(900) + const [attempt, setAttempt] = createSignal(0) + const [active, setActive] = createSignal() + const [copied, setCopied] = createSignal("") + const ret = () => safeReturn(params().get("return")) + const profile = () => page(params(), "/profile") + + function abort() { + active()?.abort() + setActive(undefined) + } + + function done() { + window.setTimeout(() => nav(ret(), { replace: true }), 650) + } + + function start(input: ProjectQuery | undefined = server.query()) { + if (!input) return + abort() + const ctl = new AbortController() + const rev = attempt() + 1 + setAttempt(rev) + setActive(ctl) + setAuth({ status: "initiating" }) + void startKiloLogin(input) + .then((info) => { + if (attempt() !== rev) return false + setAuth({ + status: "pending", + code: parseDeviceCode(info.instructions), + url: info.url, + expiresIn: 900, + }) + return completeKiloLogin(input, ctl.signal).then(() => true) + }) + .then((ok) => { + if (!ok || attempt() !== rev) return + setAuth({ status: "success" }) + void loadKiloProfile(input) + .then(() => markDisconnected(false)) + .catch(() => markDisconnected(false)) + .finally(done) + }) + .catch((err) => { + if (attempt() !== rev) return + setAuth({ status: "error", error: errMsg(err) }) + }) + .finally(() => { + if (active() === ctl) setActive(undefined) + }) + } + + function cancel() { + abort() + setAttempt((value) => value + 1) + setAuth({ status: "cancelled" }) + } + + function copy(label: string, value: string | undefined) { + if (!value) return + void navigator.clipboard + .writeText(value) + .then(() => { + setCopied(label) + window.setTimeout(() => { + if (copied() === label) setCopied("") + }, 1400) + }) + .catch((err) => setAuth({ status: "error", error: errMsg(err) })) + } + + function open() { + const url = auth().url + if (!url) return + window.open(url, "_blank", "noopener,noreferrer") + } + + createEffect(() => { + const current = server.query() + if (!current || auth().status !== "idle") return + start(current) + }) + + createEffect(() => { + const state = auth() + if (state.status !== "pending") return + setLeft(state.expiresIn ?? 900) + const timer = window.setInterval(() => setLeft((value) => Math.max(0, value - 1)), 1000) + onCleanup(() => window.clearInterval(timer)) + }) + + createEffect(() => { + if (auth().status === "success") server.remember() + }) + + onCleanup(() => { + abort() + setAttempt((value) => value + 1) + }) + + return ( +
+ +
+ ) +} diff --git a/packages/kilo-console/src/routes/profile/ProfileRoute.tsx b/packages/kilo-console/src/routes/profile/ProfileRoute.tsx index 90d20f8a16..71a74ac6aa 100644 --- a/packages/kilo-console/src/routes/profile/ProfileRoute.tsx +++ b/packages/kilo-console/src/routes/profile/ProfileRoute.tsx @@ -1,9 +1,315 @@ +import { A, useLocation, useNavigate } from "@solidjs/router" +import { Button } from "@kilocode/kilo-web-ui/button" +import { Card } from "@kilocode/kilo-web-ui/card" +import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js" +import { LoadingScreen } from "../../components/LoadingScreen" +import { loadKiloProfile, logoutKilo, setKiloOrganization, type KiloProfileData, type ProjectQuery } from "../../client" +import { errMsg } from "../../shared/utils" +import { + authError, + cloud, + credits, + initials, + markDisconnected, + money, + page, + personal, + usage, + wasDisconnected, +} from "./profile-utils" +import { useProfileServer } from "./server" + +type Org = NonNullable[number] +type State = { kind: "connected"; data: KiloProfileData } | { kind: "disconnected" } + +async function load(input: ProjectQuery): Promise { + if (wasDisconnected()) return { kind: "disconnected" } + try { + const data = await loadKiloProfile(input) + markDisconnected(false) + return { kind: "connected", data } + } catch (err) { + if (authError(err)) { + markDisconnected(true) + return { kind: "disconnected" } + } + throw err + } +} + +function org(data: KiloProfileData | undefined) { + if (!data?.currentOrgId) return undefined + return data.profile.organizations?.find((item) => item.id === data.currentOrgId) +} + +function account(data: KiloProfileData | undefined) { + return org(data)?.name ?? "Personal Account" +} + +function role(input: Org) { + if (input.role === "owner") return "Owner" + if (input.role === "admin") return "Admin" + if (input.role === "billing_manager") return "Billing" + return "Member" +} + export function ProfileRoute() { + const loc = useLocation() + const nav = useNavigate() + const params = createMemo(() => new URLSearchParams(loc.search)) + const server = useProfileServer(params) + const [data, actions] = createResource(server.query, load) + const [saving, setSaving] = createSignal() + const [error, setError] = createSignal("") + const profile = createMemo(() => { + const state = data() + if (state?.kind === "connected") return state.data + return undefined + }) + const orgs = createMemo(() => profile()?.profile.organizations ?? []) + const active = createMemo(() => org(profile())) + const disconnected = createMemo(() => data()?.kind === "disconnected") + const scope = createMemo(() => { + if (profile()) return account(profile()) + if (data.loading) return "Loading..." + return "Not connected" + }) + const login = () => page(params(), "/kilo/login") + const overview = () => page(params(), "/profile") + const usageUrl = createMemo(() => usage(profile()?.currentOrgId)) + const creditsUrl = createMemo(() => credits(profile()?.currentOrgId)) + + createEffect(() => { + if (profile()) server.remember() + }) + + createEffect(() => { + const err = data.error + if (!err || authError(err)) return + server.recover() + }) + + function refresh() { + setError("") + void actions.refetch() + } + + function choose(id: string | null) { + const current = server.query() + if (!current) return + const next = id ?? personal + const selected = profile()?.currentOrgId ?? null + if (selected === id) return + setSaving(next) + setError("") + void setKiloOrganization(current, id) + .then(() => actions.refetch()) + .catch((err) => setError(errMsg(err))) + .finally(() => setSaving(undefined)) + } + + function logout() { + const current = server.query() + if (!current) return + setSaving("logout") + setError("") + void logoutKilo(current) + .then(() => { + markDisconnected(true) + actions.mutate({ kind: "disconnected" }) + nav(overview(), { replace: true }) + }) + .catch((err) => setError(errMsg(err))) + .finally(() => setSaving(undefined)) + } + return ( -
-

Profile

-

Profile Settings

-

Profile identity, account preferences, and workspace defaults will live here.

+
+ + + +
+
+ +
+
+

Kilo Account

+

Your Profile

+

Manage your Kilo identity, account context, credits, and billing shortcuts.

+
+
+ + + Open Dashboard + +
+
+
+ + + + + + + + + + + + +
+

Kilo Account

+

Connect your Kilo account

+

Sign in to view your credits, organizations, and account details in Kilo Console.

+
+ + Connect + +
+
+ + + + Kilo server not found + Start Kilo Console from a running Kilo server or pass a server URL with ?server=. + + + + + + Profile request failed + {errMsg(data.error)} + + + + + + Account update failed + {error()} + + + + + {(info) => ( +
+ + + + Remaining Credits + {money(info().balance?.balance)} +

+ Credits shown for {account(info())}. Switch accounts below to see organization balances when + available. +

+
+ + +
+
+ Organizations +

Your Accounts

+
+ {orgs().length === 1 ? "1 org" : `${orgs().length} orgs`} +
+
+ + + {(item) => ( + + )} + +
+
+
+ )} +
+
+
) } diff --git a/packages/kilo-console/src/routes/profile/profile-utils.test.ts b/packages/kilo-console/src/routes/profile/profile-utils.test.ts new file mode 100644 index 0000000000..95ed86a0e9 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/profile-utils.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import { authError, credits, initials, money, page, parseDeviceCode, safeReturn, usage } from "./profile-utils" + +test("parses device auth codes from instructions", () => { + expect(parseDeviceCode("Open https://app.kilo.ai/device-auth and enter code: ABCD-2345")).toBe("ABCD-2345") + expect(parseDeviceCode("Use ABCD-2345 to continue")).toBe("ABCD-2345") + expect(parseDeviceCode(undefined)).toBeUndefined() +}) + +test("detects Kilo auth failures", () => { + expect(authError(new Error("Kilo profile: Unauthorized"))).toBe(true) + expect(authError({ status: 401 })).toBe(true) + expect(authError(new Error("Kilo profile: temporary network failure"))).toBe(false) +}) + +test("formats account display helpers", () => { + expect(money(12.5)).toBe("$12.50") + expect(money(null)).toBe("Unknown") + expect(initials("Jane Developer", "jane@example.com")).toBe("JD") + expect(initials("", "solo@example.com")).toBe("SE") +}) + +test("keeps login returns internal", () => { + expect(safeReturn("/profile?server=http%3A%2F%2F127.0.0.1%3A4097")).toBe( + "/profile?server=http%3A%2F%2F127.0.0.1%3A4097", + ) + expect(safeReturn("https://app.kilo.ai/profile")).toBe("/profile") + expect(safeReturn("//app.kilo.ai/profile")).toBe("/profile") +}) + +test("builds local links while preserving server", () => { + const params = new URLSearchParams({ server: "http://127.0.0.1:4097", ignored: "1" }) + expect(page(params, "/kilo/login", { return: "/profile" })).toBe( + "/kilo/login?server=http%3A%2F%2F127.0.0.1%3A4097&return=%2Fprofile", + ) +}) + +test("builds account-aware cloud links", () => { + const id = "9d4b144c-0a2b-477b-973d-24fa02bebf13" + expect(usage(null)).toBe("https://app.kilo.ai/usage") + expect(usage(id)).toBe(`https://app.kilo.ai/organizations/${id}/usage-details`) + expect(credits(undefined)).toBe("https://app.kilo.ai/profile") + expect(credits(id)).toBe(`https://app.kilo.ai/organizations/${id}`) +}) diff --git a/packages/kilo-console/src/routes/profile/profile-utils.ts b/packages/kilo-console/src/routes/profile/profile-utils.ts new file mode 100644 index 0000000000..b61e6e0347 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/profile-utils.ts @@ -0,0 +1,80 @@ +export const personal = "personal" + +let marked = false + +export function markDisconnected(input: boolean) { + marked = input +} + +export function wasDisconnected() { + return marked +} + +function text(input: unknown) { + if (input instanceof Error) return input.message + if (typeof input === "string") return input + if (input === undefined || input === null) return "" + return JSON.stringify(input) +} + +export function parseDeviceCode(input: string | undefined) { + if (!input) return undefined + const code = input.match(/code:\s*([A-Z0-9-]+)/i)?.[1] + if (code) return code.toUpperCase() + return input.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i)?.[1]?.toUpperCase() +} + +export function authError(input: unknown) { + const value = text(input).toLowerCase() + return value.includes("unauthorized") || value.includes("invalid token") || value.includes('status":401') +} + +export function money(input: number | null | undefined) { + if (typeof input !== "number" || !Number.isFinite(input)) return "Unknown" + return `$${input.toFixed(2)}` +} + +export function initials(name: string | undefined, email: string) { + const parts = (name?.trim() || email) + .split(/[\s._@-]+/) + .filter(Boolean) + .slice(0, 2) + const value = parts.map((part) => part[0] ?? "").join("") + return value.toUpperCase() || "KG" +} + +export function safeReturn(input: string | null | undefined) { + if (!input || !input.startsWith("/") || input.startsWith("//")) return "/profile" + try { + const url = new URL(input, "http://localhost") + if (url.origin !== "http://localhost") return "/profile" + return `${url.pathname}${url.search}${url.hash}` + } catch { + return "/profile" + } +} + +export function page(params: URLSearchParams, path: string, extra?: Record) { + const next = new URLSearchParams() + const server = params.get("server") + if (server) next.set("server", server) + for (const [key, value] of Object.entries(extra ?? {})) { + if (value) next.set(key, value) + } + const query = next.toString() + return `${path}${query ? `?${query}` : ""}` +} + +export function cloud(path = "/profile") { + return `https://app.kilo.ai${path}` +} + +export function usage(id: string | null | undefined) { + if (!id) return cloud("/usage") + return cloud(`/organizations/${encodeURIComponent(id)}/usage-details`) +} + +export function credits(id: string | null | undefined) { + if (!id) return cloud("/profile") + return cloud(`/organizations/${encodeURIComponent(id)}`) +} diff --git a/packages/kilo-console/src/routes/profile/server.ts b/packages/kilo-console/src/routes/profile/server.ts new file mode 100644 index 0000000000..82c748a618 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/server.ts @@ -0,0 +1,71 @@ +import { createEffect, createMemo, createSignal } from "solid-js" +import { + discover, + forgetCached, + loadCached, + resolveServer, + saveCached, + type ProjectQuery, +} from "../../client" +import { clean } from "../../shared/utils" + +const ports = new Set(["3017", "3018"]) + +function shouldDiscover(input: URLSearchParams) { + if (input.get("server")) return false + return ports.has(window.location.port) +} + +function base(input: URLSearchParams) { + const param = input.get("server") + if (param) return param + const cached = shouldDiscover(input) ? loadCached() : "" + if (cached) return cached + if (shouldDiscover(input)) return "" + return window.location.origin +} + +export function useProfileServer(params: () => URLSearchParams) { + const [url, setUrl] = createSignal(base(params())) + const discoverable = () => shouldDiscover(params()) + const query = createMemo(() => { + const target = clean(url()) || base(params()) + if (!target) return undefined + return { url: target, dir: "" } + }) + + createEffect(() => { + const next = params().get("server") + if (next && next !== url()) setUrl(next) + }) + + createEffect(() => { + if (!discoverable()) return + void resolveServer().then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + }) + + function remember() { + const current = query() + if (!current || !discoverable()) return + saveCached(current.url) + } + + function recover() { + if (!discoverable()) return + const cached = loadCached() + if (!cached || cached !== url()) return + forgetCached() + setUrl("") + void discover().then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + } + + return { query, discoverable, remember, recover } +} diff --git a/packages/kilo-console/src/shared/navigation.test.ts b/packages/kilo-console/src/shared/navigation.test.ts index 01f36705c1..53f6682cea 100644 --- a/packages/kilo-console/src/shared/navigation.test.ts +++ b/packages/kilo-console/src/shared/navigation.test.ts @@ -11,6 +11,7 @@ test("classifies routes after stripping the console base", () => { expect(path("/console/projects/demo/settings/agents", "/console")).toBe("/project") expect(path("/console/settings/agents", "/console")).toBe("/settings") expect(path("/console/profile", "/console")).toBe("/profile") + expect(path("/console/kilo/login", "/console")).toBe("/profile") }) test("builds settings roots without preserving the console base", () => { diff --git a/packages/kilo-console/src/shared/navigation.ts b/packages/kilo-console/src/shared/navigation.ts index 3d1476eebe..195f491a0a 100644 --- a/packages/kilo-console/src/shared/navigation.ts +++ b/packages/kilo-console/src/shared/navigation.ts @@ -21,7 +21,7 @@ export function settings(input: string, prefix = base()) { export function path(input: string, prefix = base()): Path { const route = strip(input, prefix) - if (route === "/profile") return "/profile" + if (route === "/profile" || route.startsWith("/kilo/login")) return "/profile" if (route.startsWith("/settings") || route.startsWith("/config")) return "/settings" if (route.startsWith("/projects/")) return "/project" return "/projects" diff --git a/packages/kilo-console/src/styles.css b/packages/kilo-console/src/styles.css index 5ab4a61da3..bad09eb3ec 100644 --- a/packages/kilo-console/src/styles.css +++ b/packages/kilo-console/src/styles.css @@ -14,6 +14,7 @@ @import "./styles/agents-tools.css"; @import "./styles/projects.css"; @import "./styles/project-console.css"; +@import "./styles/profile.css"; @import "./styles/servers.css"; @import "./styles/sources.css"; @import "./styles/responsive.css"; diff --git a/packages/kilo-console/src/styles/models.css b/packages/kilo-console/src/styles/models.css index 626f43786a..6f12e42d1b 100644 --- a/packages/kilo-console/src/styles/models.css +++ b/packages/kilo-console/src/styles/models.css @@ -272,6 +272,10 @@ flex-wrap: wrap; } +.kilo-console .models-capabilities-list button { + text-transform: capitalize; +} + .kilo-console .explore-models { grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr)); gap: 0.625rem; diff --git a/packages/kilo-console/src/styles/profile.css b/packages/kilo-console/src/styles/profile.css new file mode 100644 index 0000000000..c5630cfa28 --- /dev/null +++ b/packages/kilo-console/src/styles/profile.css @@ -0,0 +1,480 @@ +.kilo-console .profile-page, +.kilo-console .profile-login-page { + display: grid; + gap: 1rem; + max-width: 64rem; + margin: 0 auto; +} + +.kilo-console .profile-login-page { + max-width: 34rem; +} + +.kilo-console .profile-shell.disconnected { + grid-template-columns: minmax(0, 1fr); +} + +.kilo-console .profile-header, +.kilo-console .profile-login-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1rem; + align-items: start; + padding-bottom: 0.5rem; +} + +.kilo-console .profile-login-header { + grid-template-columns: 1fr; + text-align: center; +} + +.kilo-console .profile-header h1, +.kilo-console .profile-login-header h1 { + margin: 0; + color: var(--foreground); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.kilo-console .profile-header p:not(.eyebrow), +.kilo-console .profile-login-header p:not(.eyebrow) { + max-width: 42rem; + margin: 0.625rem 0 0; + color: var(--muted-foreground); + font-size: 0.8125rem; + line-height: 1.45; +} + +.kilo-console .profile-actions, +.kilo-console .profile-card-actions, +.kilo-console .profile-login-actions { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; +} + +.kilo-console .profile-primary-link { + text-decoration: none; +} + +.kilo-console .profile-connect-card { + display: grid; + min-height: 18rem; + place-items: center; + align-content: center; + gap: 1.25rem; + text-align: center; +} + +.kilo-console .profile-connect-mark { + display: grid; + width: 3.5rem; + height: 3.5rem; + place-items: center; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: + linear-gradient(135deg, color-mix(in oklab, var(--primary) 32%, transparent), transparent), + color-mix(in oklab, var(--muted) 70%, transparent); + color: var(--foreground); + font-size: 1rem; + font-weight: 700; + letter-spacing: 0.08em; +} + +.kilo-console .profile-connect-copy { + display: grid; + max-width: 28rem; + gap: 0.625rem; +} + +.kilo-console .profile-connect-copy h1, +.kilo-console .profile-connect-copy p { + margin: 0; +} + +.kilo-console .profile-connect-copy h1 { + color: var(--foreground); + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.kilo-console .profile-connect-copy p:not(.eyebrow) { + color: var(--muted-foreground); + font-size: 0.8125rem; + line-height: 1.5; +} + +.kilo-console .profile-link-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--input-base); + color: var(--foreground); + font-size: 0.75rem; + font-weight: 500; + line-height: 1.45; + padding: 0.25rem 0.625rem; + text-decoration: none; + transition: + background 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +.kilo-console .profile-link-button:hover, +.kilo-console .profile-link-button:focus-visible { + border-color: var(--ring); + background: var(--muted); + color: var(--foreground); + outline: 0; +} + +.kilo-console .profile-banner { + margin-bottom: 0; +} + +.kilo-console .profile-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.kilo-console .profile-card { + gap: 0.875rem; +} + +.kilo-console .profile-wide-card { + grid-column: 1 / -1; +} + +.kilo-console .profile-account-card, +.kilo-console .profile-balance-card { + min-height: 10rem; +} + +.kilo-console .profile-account-head { + display: flex; + gap: 0.875rem; + align-items: center; + min-width: 0; +} + +.kilo-console .profile-avatar, +.kilo-console .profile-org-icon { + display: grid; + flex: 0 0 auto; + place-items: center; + border: 1px solid var(--border); + background: + linear-gradient(135deg, color-mix(in oklab, var(--primary) 28%, transparent), transparent), + color-mix(in oklab, var(--muted) 70%, transparent); + color: var(--foreground); + font-weight: 700; + letter-spacing: 0.06em; +} + +.kilo-console .profile-avatar { + width: 3.25rem; + height: 3.25rem; + border-radius: var(--radius-lg); + font-size: 1rem; +} + +.kilo-console .profile-org-icon { + width: 2rem; + height: 2rem; + border-radius: var(--radius-sm); + font-size: 0.6875rem; +} + +.kilo-console .profile-account-text { + display: grid; + min-width: 0; + gap: 0.125rem; +} + +.kilo-console .profile-account-text strong, +.kilo-console .profile-card-head h2, +.kilo-console .profile-login-state strong, +.kilo-console .profile-login-step strong { + margin: 0; + color: var(--foreground); + font-size: 0.9375rem; + font-weight: 600; + line-height: 1.25; +} + +.kilo-console .profile-account-text span, +.kilo-console .profile-login-state p, +.kilo-console .profile-login-step > span, +.kilo-console .profile-login-waiting { + color: var(--muted-foreground); + font-size: 0.75rem; + line-height: 1.45; +} + +.kilo-console .profile-account-text span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-meta-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: color-mix(in oklab, var(--muted) 35%, transparent); + padding: 0.75rem; +} + +.kilo-console .profile-meta-row span, +.kilo-console .profile-card-label { + color: var(--muted-foreground); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.kilo-console .profile-meta-row strong { + color: var(--foreground); + font-size: 0.75rem; + font-weight: 600; +} + +.kilo-console .profile-balance-card strong { + color: var(--foreground); + font-size: clamp(2rem, 6vw, 3.25rem); + font-weight: 700; + letter-spacing: -0.05em; + line-height: 0.95; +} + +.kilo-console .profile-balance-card p, +.kilo-console .profile-login-state p { + margin: 0; +} + +.kilo-console .profile-card-head { + display: flex; + gap: 1rem; + align-items: flex-start; + justify-content: space-between; +} + +.kilo-console .profile-pill { + display: inline-flex; + align-items: center; + min-height: 1.5rem; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--muted-foreground); + font-size: 0.6875rem; + font-weight: 600; + padding: 0 0.625rem; + text-decoration: none; + text-transform: uppercase; +} + +.kilo-console .profile-org-list { + display: grid; + gap: 0.5rem; +} + +.kilo-console .profile-org-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + width: 100%; + min-height: 3.5rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--card); + color: var(--foreground); + cursor: pointer; + font: inherit; + padding: 0.75rem; + text-align: left; + transition: + background 120ms ease, + border-color 120ms ease; +} + +.kilo-console .profile-org-row:hover, +.kilo-console .profile-org-row:focus-visible, +.kilo-console .profile-org-row.active { + border-color: var(--ring); + background: color-mix(in oklab, var(--muted) 45%, transparent); + outline: 0; +} + +.kilo-console .profile-org-row:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +.kilo-console .profile-org-body { + display: grid; + min-width: 0; + gap: 0.125rem; +} + +.kilo-console .profile-org-body strong { + overflow: hidden; + color: var(--foreground); + font-size: 0.8125rem; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-org-body span, +.kilo-console .profile-org-state { + color: var(--muted-foreground); + font-size: 0.6875rem; +} + +.kilo-console .profile-org-state { + font-weight: 600; +} + +.kilo-console .profile-login-card { + min-height: 18rem; +} + +.kilo-console .profile-login-state, +.kilo-console .profile-login-flow, +.kilo-console .profile-login-step { + display: grid; + gap: 0.875rem; +} + +.kilo-console .profile-login-state { + place-items: center; + min-height: 14rem; + text-align: center; +} + +.kilo-console .profile-login-state.error strong { + color: var(--destructive); +} + +.kilo-console .profile-login-state.success strong { + color: var(--success, var(--primary)); +} + +.kilo-console .profile-login-step { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: color-mix(in oklab, var(--muted) 25%, transparent); + padding: 1rem; +} + +.kilo-console .profile-login-url { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.5rem; + align-items: center; +} + +.kilo-console .profile-login-url span { + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--input-base); + color: var(--foreground); + font-family: var(--font-family-mono, monospace); + font-size: 0.75rem; + padding: 0.5rem 0.625rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-login-code { + display: grid; + gap: 0.25rem; + place-items: center; + border: 1px solid var(--ring); + border-radius: var(--radius-lg); + background: color-mix(in oklab, var(--primary) 12%, var(--input-base)); + color: var(--foreground); + cursor: pointer; + font-family: var(--font-family-mono, monospace); + font-size: 2rem; + font-weight: 700; + letter-spacing: 0.16em; + padding: 1rem; +} + +.kilo-console .profile-login-code small { + color: var(--muted-foreground); + font-family: var(--font-family-sans, system-ui, sans-serif); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0; +} + +.kilo-console .profile-login-waiting { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.kilo-console .profile-spinner { + width: 1rem; + height: 1rem; + border: 2px solid color-mix(in oklab, var(--muted-foreground) 30%, transparent); + border-top-color: var(--primary); + border-radius: 999px; + animation: profile-spin 900ms linear infinite; +} + +@keyframes profile-spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 760px) { + .kilo-console .profile-header, + .kilo-console .profile-grid { + grid-template-columns: 1fr; + } + + .kilo-console .profile-actions, + .kilo-console .profile-card-actions, + .kilo-console .profile-login-actions { + justify-content: flex-start; + } + + .kilo-console .profile-login-url { + grid-template-columns: 1fr; + } +} + +@media (max-width: 520px) { + .kilo-console .profile-org-row { + grid-template-columns: auto minmax(0, 1fr); + } + + .kilo-console .profile-org-state { + grid-column: 2; + } + + .kilo-console .profile-login-code { + font-size: 1.5rem; + } +} diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 59afaa05d6..7ba653f7c0 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.3.48", + "version": "7.3.49", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md index 189db05657..5d9fc1fcfd 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md @@ -11,6 +11,7 @@ Auto Model is a smart routing system that selects an underlying model for each r |---|---|---| | `kilo-auto/frontier` | Maximum capability with the best available models | Paid | | `kilo-auto/balanced` | Strong performance at a lower cost | Paid | +| `kilo-auto/efficient` | Lowest cost per task, with capability matched to difficulty | Paid | | `kilo-auto/free` | The best free models available | Free | ## How It Works @@ -31,8 +32,17 @@ The underlying models behind each Auto Model tier are updated server-side as bet - **Frontier** โ€” Routes to the latest and most capable paid models. Uses different models for reasoning-heavy tasks (planning, architecture, debugging) versus implementation tasks (coding, building, exploring), pairing the right capability to each type of work. - **Balanced** โ€” Routes to a cost-effective model for all modes. The specific model is selected based on the API interface in use, but does not vary by mode. A good default for most developers who want strong AI assistance without paying frontier prices. +- **Efficient** โ€” Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to the Balanced tier, so quality never drops below Balanced. - **Free** โ€” Routes to the best available free models on OpenRouter, splitting traffic across them. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side โ€” you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the models may change over time. +### How Auto Efficient routing works + +1. Kilo observes your coding session in context +2. It classifies each task by complexity +3. It routes to the benchmark-proven best model for that task automatically + +You get lean costs on routine work and stronger models when the work demands it โ€” with no manual switching. + {% callout type="warning" title="Data handling for Auto Free" %} Auto Free may route your requests to providers that log prompts and outputs and use them to improve their services. Do not submit personal or confidential data when using Auto Free. In particular, it may route to NVIDIA's free endpoints. @@ -51,7 +61,7 @@ No need to manually switch models when changing modes. Auto Model handles routin ### Flexible Cost Control -Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Free costs nothing. +Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Efficient minimizes cost per task by matching model capability to task difficulty; Free costs nothing. ## Requirements diff --git a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md index 710f38bfee..b187da64dc 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md @@ -60,7 +60,7 @@ While the specifics change constantly, some principles stay consistent: ## Free and Budget Model Picks -You don't need a paid API key to use Kilo Code productively. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup. +You don't need a paid API key to use Kilo Code productively. For the lowest cost on paid work, [Auto Efficient](/docs/code-with-ai/agents/auto-model#tiers) (`kilo-auto/efficient`) routes each request to the cheapest model proven accurate enough for that task. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup. If you prefer to pick models yourself, type `free` in the model picker to filter by free models, or browse the full list at [kilo.ai/models](https://kilo.ai/models). diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index 1243bbb454..adb426172d 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -97,5 +97,6 @@ Your IAM user or role must have the following permissions: ## Using BYOK in the Extensions and CLI - BYOK works with the Kilo Gateway provider. Users should ensure that is set as the active [provider](/docs/ai-providers). -- Select a model from a provider configured for BYOK, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan. +- Kilo Gateway models that can use one of your enabled personal or organization BYOK providers display a `BYOK` badge in the model picker. The badge does not apply to models selected through other providers. +- Select a model with the `BYOK` badge, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan. - (Optional) Validate with the provider that traffic is being served by that key. diff --git a/packages/kilo-docs/pages/getting-started/settings/index.md b/packages/kilo-docs/pages/getting-started/settings/index.md index a3e9ddd536..9c2a9be2be 100644 --- a/packages/kilo-docs/pages/getting-started/settings/index.md +++ b/packages/kilo-docs/pages/getting-started/settings/index.md @@ -59,6 +59,18 @@ When the Kilo provider is enabled and you are signed in, choose the transcriptio } ``` +### Prompt-Training Model Visibility + +Enable **Hide Prompt-Training Models** under **Models** to remove Kilo Gateway models whose providers may use your prompts for training from model lists. Models from other providers and models without explicit prompt-training metadata remain visible. The setting is disabled by default. + +You can also enable it in `kilo.jsonc`: + +```json +{ + "hide_prompt_training_models": true +} +``` + ### Reasoning Blocks Reasoning blocks stay expanded by default in the VS Code chat UI. Enable **Auto-Collapse Reasoning** in the Display tab, or set `auto_collapse_reasoning` in `kilo.jsonc`, to collapse them after the agent finishes writing them: diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png index 728ac2a184..a4234ebfc9 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6ecf78a0bcfaebdb945c3caacb8e32155fe2f6e4017f766e129dd0d17ad8fab -size 51349 +oid sha256:270d3ecab98c4462a0e6af3e8fe43b3313da03759c507914d0b1048045474f1e +size 50886 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 5a30e68d6e..49c8e1c151 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -81,8 +81,6 @@ - -- - - - diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index 8cb6c4bab7..26418f3adc 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.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index d59fb219fc..598255e66a 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -37,6 +37,7 @@ const openRouterModelSchema = z.object({ preferredIndex: z.number().optional(), isFree: z.boolean().optional(), mayTrainOnYourPrompts: z.boolean().optional(), + hasUserByokAvailable: z.boolean().optional(), terminalBench: z .object({ overallScore: z.number(), @@ -192,6 +193,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any { tool_call: supportsTools, isFree: model.isFree, mayTrainOnYourPrompts: model.mayTrainOnYourPrompts, + hasUserByokAvailable: model.hasUserByokAvailable, ...(model.terminalBench && { terminalBench: model.terminalBench }), ...(inputPrice !== undefined && outputPrice !== undefined && { diff --git a/packages/kilo-gateway/src/provider.ts b/packages/kilo-gateway/src/provider.ts index b0a54bded8..1f52b09c2d 100644 --- a/packages/kilo-gateway/src/provider.ts +++ b/packages/kilo-gateway/src/provider.ts @@ -9,7 +9,7 @@ import { getApiKey } from "./auth/token.js" import { buildKiloHeaders, getDefaultHeaders } from "./headers.js" import { ANONYMOUS_API_KEY } from "./api/constants.js" import { resolveKiloOpenRouterBaseUrl } from "./api/url.js" -import { sanitizeResponsesBody } from "./responses.js" +import { transformRequestBody } from "./responses.js" export function buildRequestHeaders(defaultHeaders: Record, requestHeaders?: HeadersInit): Headers { const headers = new Headers(defaultHeaders) @@ -55,7 +55,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider { const originalFetch = options.fetch ?? fetch const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => { const headers = buildRequestHeaders(customHeaders, init?.headers) - const body = sanitizeResponsesBody(input, init?.body) + const body = transformRequestBody(input, init?.body, options.dataCollection) // Add authorization if API key exists if (apiKey) { diff --git a/packages/kilo-gateway/src/responses.ts b/packages/kilo-gateway/src/responses.ts index 3697c5cf77..5ba68266ed 100644 --- a/packages/kilo-gateway/src/responses.ts +++ b/packages/kilo-gateway/src/responses.ts @@ -28,8 +28,13 @@ function strip(input: unknown[]) { return { kept, changed } } -export function sanitizeResponsesBody(input: string | URL | Request, body: BodyInit | null | undefined) { - if (!endpoint(input)) return body +export function transformRequestBody( + input: string | URL | Request, + body: BodyInit | null | undefined, + value?: "allow" | "deny", +) { + const responses = endpoint(input) + if (!responses && !value) return body if (typeof body !== "string") return body const data = (() => { @@ -40,10 +45,14 @@ export function sanitizeResponsesBody(input: string | URL | Request, body: BodyI } })() if (!record(data)) return body - if (data.store === true) return body - if (!Array.isArray(data.input)) return body - const result = strip(data.input) - if (!result.changed) return body - return JSON.stringify({ ...data, input: result.kept }) + const result = responses && data.store !== true && Array.isArray(data.input) ? strip(data.input) : undefined + if (!result?.changed && !value) return body + + const provider = record(data.provider) ? data.provider : {} + return JSON.stringify({ + ...data, + ...(result?.changed ? { input: result.kept } : {}), + ...(value ? { provider: { ...provider, data_collection: value } } : {}), + }) } diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index e9e8080f02..ab214d3274 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -95,6 +95,11 @@ export interface KiloProviderOptions { */ name?: string + /** + * Data collection preference for upstream provider routing + */ + dataCollection?: "allow" | "deny" + /** * Custom fetch function */ diff --git a/packages/kilo-gateway/test/api/models.test.ts b/packages/kilo-gateway/test/api/models.test.ts index 10ce9ecedb..56a589af0e 100644 --- a/packages/kilo-gateway/test/api/models.test.ts +++ b/packages/kilo-gateway/test/api/models.test.ts @@ -17,6 +17,7 @@ const VALID_RESPONSE = JSON.stringify({ supported_parameters: ["tools", "temperature"], isFree: false, mayTrainOnYourPrompts: true, + hasUserByokAvailable: true, }, ], }) @@ -148,6 +149,7 @@ test("returns models without error on success", async () => { expect(result.models["test/model-a"]).toMatchObject({ isFree: false, mayTrainOnYourPrompts: true, + hasUserByokAvailable: true, }) }) diff --git a/packages/kilo-gateway/test/responses.test.ts b/packages/kilo-gateway/test/responses.test.ts index 49520300f5..9d73d65c78 100644 --- a/packages/kilo-gateway/test/responses.test.ts +++ b/packages/kilo-gateway/test/responses.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { sanitizeResponsesBody } from "../src/responses" +import { transformRequestBody } from "../src/responses" describe("Responses request sanitization", () => { test("strips item ids when storage is disabled", () => { @@ -30,7 +30,7 @@ describe("Responses request sanitization", () => { ], }) - const result = sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body) const data = JSON.parse(result as string) expect(data.input).toHaveLength(3) @@ -60,19 +60,19 @@ describe("Responses request sanitization", () => { ], }) - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("leaves non-responses requests unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) }) test("leaves invalid responses JSON unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("matches relative responses paths without a placeholder host", () => { @@ -86,9 +86,32 @@ describe("Responses request sanitization", () => { }, ], }) - const result = sanitizeResponsesBody("/api/openrouter/responses?stream=true", body) + const result = transformRequestBody("/api/openrouter/responses?stream=true", body) const data = JSON.parse(result as string) expect(data.input[0].id).toBeUndefined() }) + + test("sanitizes responses and denies data collection in one transform", () => { + const body = JSON.stringify({ + input: [{ type: "message", role: "assistant", id: "msg_tmp_123" }], + provider: { order: ["anthropic"] }, + }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + input: [{ type: "message", role: "assistant" }], + provider: { order: ["anthropic"], data_collection: "deny" }, + }) + }) + + test("denies data collection for non-responses requests", () => { + const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + model: "anthropic/claude-sonnet-4", + provider: { data_collection: "deny" }, + }) + }) }) diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 9688347cbf..77c43e2a42 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.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index 828fc54dcd..6df3d29341 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-indexing", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "description": "Standalone indexing engine and host helpers for Kilo Code", diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 777f414b95..bad3c6fd29 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -88,6 +88,23 @@ ## [Unreleased] +## [7.0.1-rc.12] - 2026-06-18 + +### Added + +- Provider settings management, including searchable provider lists, API-key configuration, OAuth provider login, provider enable/disable controls, disconnect actions, and shared provider metadata. +- Add copy controls to session messages so prompts and assistant responses can be copied directly from the transcript. +- Share codebase indexes across worktrees so Agent Manager and worktree sessions can use semantic search without duplicating the full index. + +### Fixed + +- Keep long JetBrains prompt input usable by capping growth, preserving scrolling, and hiding soft-wrap glyphs. +- Copy actions correctly in session. + +### Changed + +- Update the bundled CLI runtime to OpenCode 1.15.9 + ## [7.0.1-rc.11] - 2026-06-17 ### Added diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index 02a63765b3..4fa7e4b608 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -988,6 +988,7 @@ object KiloCliDataParser { temperature = model.temperature, toolCall = model.toolCall, free = model.free, + byok = model.byok, status = model.status, recommendedIndex = model.recommendedIndex, variants = model.variants, @@ -1006,6 +1007,7 @@ object KiloCliDataParser { temperature = cap.bool("temperature"), toolCall = cap.bool("toolcall"), free = obj.bool("isFree"), + byok = obj.bool("hasUserByokAvailable"), status = obj.str("status"), recommendedIndex = obj.num("recommendedIndex"), variants = parseVariants(obj), diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt index fc6c4246b9..c1c604285c 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt @@ -74,6 +74,7 @@ internal object KiloWorkspaceDtoMapper { temperature = m.temperature, toolCall = m.toolCall, free = m.free, + byok = m.byok, status = m.status, recommendedIndex = m.recommendedIndex, variants = m.variants, diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt index b21b4bf707..20256e96f5 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt @@ -54,6 +54,7 @@ data class ModelInfo( val temperature: Boolean, val toolCall: Boolean, val free: Boolean, + val byok: Boolean = false, val status: String?, val recommendedIndex: Double?, val variants: List, diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt index b19ed6072d..a457d30883 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt @@ -128,7 +128,8 @@ class GeneratedApiModelSerializationTest { "options": {}, "headers": {}, "release_date": "2025-01-01", - "isFree": true + "isFree": true, + "hasUserByokAvailable": true } } }], @@ -139,6 +140,7 @@ class GeneratedApiModelSerializationTest { val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! assertEquals(true, model.isFree) + assertEquals(true, model.hasUserByokAvailable) assertEquals( ai.kilocode.jetbrains.api.model.Model.Status.BETA, model.status, diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 5c824749af..ce84b208ee 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -1254,6 +1254,8 @@ class KiloCliDataParserTest { }, "limit": {"context": 200000, "input": 100000, "output": 16000}, "status": "active", + "isFree": false, + "hasUserByokAvailable": true, "recommendedIndex": 2, "variants": {"high": {}, "low": {}, "medium": {}}, "options": {}, "headers": {} @@ -1273,6 +1275,8 @@ class KiloCliDataParserTest { assertTrue(model.temperature) assertTrue(model.toolCall) assertEquals("active", model.status) + assertFalse(model.free) + assertTrue(model.byok) assertEquals(2.0, model.recommendedIndex) assertEquals(200000L, model.limit?.context) assertEquals(100000L, model.limit?.input) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt index 35ba6f52e3..d9ad9a7c96 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt @@ -111,7 +111,8 @@ class ProjectModelSerializationTest { "options": {}, "headers": {}, "release_date": "2025-01-01", - "isFree": true + "isFree": true, + "hasUserByokAvailable": true } } }], @@ -122,6 +123,7 @@ class ProjectModelSerializationTest { val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! assertEquals(true, model.isFree) + assertEquals(true, model.hasUserByokAvailable) assertEquals( ai.kilocode.jetbrains.api.model.Model.Status.BETA, model.status, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 8a47e19af4..1865e11e43 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -404,6 +404,7 @@ class SessionUi( it.providerName, it.recommendedIndex, it.free, + it.byok, it.variants, it.attachment, ) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 7048f8e9aa..27eea93327 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -663,6 +663,7 @@ class SessionController( provider.name, info.recommendedIndex, info.free, + info.byok, info.variants, info.limit?.let { ModelLimitItem(it.context, it.input, it.output) }, info.attachment, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt index 79eeb432f8..f1631540bd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt @@ -583,6 +583,7 @@ data class ModelItem( val providerName: String, val recommendedIndex: Double?, val free: Boolean, + val byok: Boolean = false, val variants: List, val limit: ModelLimitItem?, val attachment: Boolean = false, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt index a7b8efe4dd..86e767afd7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt @@ -56,6 +56,7 @@ class ModelPicker : PickerButton() { val providerName: String, val recommendedIndex: Double? = null, val free: Boolean = false, + val byok: Boolean = false, val variants: List = emptyList(), val attachment: Boolean = false, ) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index 4ce73c10a4..e3e9797dd2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -74,6 +74,14 @@ internal class ModelPickerRenderer( private val badgeLabel = BadgeLabel(badge).apply { border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) } + private val byok = FilledBadgeIcon( + "BYOK", + UiStyle.Colors.badgeBg(), + UiStyle.Colors.badgeFg(), + ) + private val byokLabel = BadgeLabel(byok).apply { + border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) + } private val warn = JBLabel(DATA_COLLECTED).apply { toolTipText = ModelText.dataCollected() border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) @@ -83,6 +91,7 @@ internal class ModelPickerRenderer( add(title) add(warn) add(badgeLabel) + add(byokLabel) add(provider) } private val star = JBLabel().apply { @@ -136,6 +145,7 @@ internal class ModelPickerRenderer( if (item == null) { title.append(value.emptyText, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) badgeLabel.isVisible = false + byokLabel.isVisible = false warn.isVisible = false provider.isVisible = false star.icon = EmptyIcon.ICON_16 @@ -150,7 +160,8 @@ internal class ModelPickerRenderer( title.append(name.model, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) warn.isVisible = ModelText.collectsData(item) - badgeLabel.isVisible = item.free + badgeLabel.isVisible = item.free && !item.byok + byokLabel.isVisible = item.byok provider.isVisible = value.favorite provider.text = item.providerName provider.foreground = weak @@ -174,6 +185,8 @@ internal class ModelPickerRenderer( internal fun badgeText(): String = badge.text + internal fun byokVisible(): Boolean = byokLabel.isVisible + internal fun warningVisible(): Boolean = warn.isVisible internal fun warningTooltip(): String? = warn.toolTipText diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt index 322e5fcfcb..3a67129f5e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt @@ -174,7 +174,16 @@ internal class ModelsSettingsUi( .filter { it.id == KILO_PROVIDER || it.id in cfg.connected } .flatMap { provider -> provider.models.mapNotNull { (id, model) -> - val item = ModelPicker.Item(id, model.name, provider.id, provider.name, model.recommendedIndex, model.free, model.variants) + val item = ModelPicker.Item( + id, + model.name, + provider.id, + provider.name, + model.recommendedIndex, + model.free, + model.byok, + model.variants, + ) if (!includeSmall && ModelText.small(item)) return@mapNotNull null item } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt index 056dcc7ba0..02d0d2fb3b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt @@ -397,6 +397,22 @@ class ModelPickerTest : BasePlatformTestCase() { assertEquals("Data may be used for training", renderer.warningTooltip()) } + fun `test renderer shows BYOK instead of free when both are available`() { + val row = ModelPickerRow( + ModelPicker.Item("claude", "Claude", "kilo", "Kilo", free = true, byok = true), + "Kilo", + false, + ) + val model = CollectionListModel(listOf(row)) + val renderer = ModelPickerRenderer(model, { null }, { emptySet() }) + val list = JBList(model) + + renderer.getListCellRendererComponent(list, row, 0, false, false) + + assertTrue(renderer.byokVisible()) + assertFalse(renderer.badgeVisible()) + } + fun `test renderer hides data collection warning for non-kilo free model`() { val row = ModelPickerRow(ModelPicker.Item("free", "Free", "openrouter", "OpenRouter", free = true), "OpenRouter", false) val model = CollectionListModel(listOf(row)) @@ -420,6 +436,7 @@ class ModelPickerTest : BasePlatformTestCase() { assertSame(EmptyIcon.ICON_16, renderer.starIcon()) assertFalse(renderer.badgeVisible()) + assertFalse(renderer.byokVisible()) } private fun item( diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index a1cdd86dc7..21af8c36d3 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -1,5 +1,5 @@ kotlin.stdlib.default.dependency=false -kilo.jetbrains.version=7.0.1-rc.11 +kilo.jetbrains.version=7.0.1-rc.12 org.gradle.configuration-cache=true org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index 21bb01694c..c5cb2716c6 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -8,7 +8,7 @@ "test": "./gradlew test", "test:ci": "bun script/test-ci.ts" }, - "version": "7.3.48", + "version": "7.3.49", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt index 6045f5c130..918b2dae51 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt @@ -11,6 +11,7 @@ data class ModelDto( val temperature: Boolean = false, val toolCall: Boolean = false, val free: Boolean = false, + val byok: Boolean = false, val status: String? = null, val recommendedIndex: Double? = null, val variants: List = emptyList(), diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index 48bbaaaa0c..ed4e1b0cc1 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.3.48", + "version": "7.3.49", "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 0b3a6b030d..f167b71a87 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-ui/src/pierre/index.test.ts b/packages/kilo-ui/src/pierre/index.test.ts index 5f2df0e951..7bc73fd3a2 100644 --- a/packages/kilo-ui/src/pierre/index.test.ts +++ b/packages/kilo-ui/src/pierre/index.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test" import { createDefaultOptions } from "./index" describe("Pierre diff options", () => { - test("highlights changed characters in unified and split diffs", () => { - expect(createDefaultOptions("unified").lineDiffType).toBe("char") - expect(createDefaultOptions("split").lineDiffType).toBe("char") + test("keeps changed identifiers intact in unified and split diffs", () => { + expect(createDefaultOptions("unified").lineDiffType).toBe("word-alt") + expect(createDefaultOptions("split").lineDiffType).toBe("word-alt") }) }) diff --git a/packages/kilo-ui/src/pierre/index.ts b/packages/kilo-ui/src/pierre/index.ts index 1df03fb88d..7b4b13aabe 100644 --- a/packages/kilo-ui/src/pierre/index.ts +++ b/packages/kilo-ui/src/pierre/index.ts @@ -10,7 +10,9 @@ import { createDefaultOptions as defaults, styleVariables } from "@opencode-ai/u export { styleVariables } -export const LINE_DIFF_TYPE = "char" as const +// Character matching fragments inserted identifiers when they share letters with +// existing symbols. Word-alt keeps those logical additions visually intact. +export const LINE_DIFF_TYPE = "word-alt" as const // Pierre 1.1 treats its changed-line override properties as tint targets. Apply // Kilo semantic surfaces at the computed row level so host diff colors stay final. diff --git a/packages/kilo-vscode/CHANGELOG.md b/packages/kilo-vscode/CHANGELOG.md index d353fb7cf8..41afeb6a1c 100644 --- a/packages/kilo-vscode/CHANGELOG.md +++ b/packages/kilo-vscode/CHANGELOG.md @@ -1,5 +1,11 @@ # kilo-code +## 7.3.49 + +### Minor Changes + +- [#11303](https://github.com/Kilo-Org/kilocode/pull/11303) [`6faa3f1`](https://github.com/Kilo-Org/kilocode/commit/6faa3f14b619144a1353f169a0a2855dcbf69bbb) - Add an "Import Sessions from Roo Code" button in the About settings tab that discovers and imports conversation history from an existing Roo Code installation, reusing the existing migration wizard. + ## 7.3.48 ### Minor Changes diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index fce6628045..602dd43c24 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.3.48", + "version": "7.3.49", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", @@ -1100,6 +1100,7 @@ "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", + "happy-dom": "20.8.9", "knip": "5.85.0", "prettier": "3.6.2", "qrcode": "^1.5.4", diff --git a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts index 189045d5a7..dbe7aa4867 100644 --- a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts +++ b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts @@ -203,8 +203,10 @@ export class SessionTerminalManager { return result } + const disposable = this.tryRegisterCommand(id, handler) + if (!disposable) return this.commandHandlers.set(id, handler) - this.commandDisposables.set(id, this.host.registerCommand(id, handler)) + this.commandDisposables.set(id, disposable) } private async runOriginalCommand(id: string, args: unknown[]): Promise { @@ -218,9 +220,20 @@ export class SessionTerminalManager { return await this.host.executeCommand(id, ...args) } finally { const handler = this.commandHandlers.get(id) - if (!handler) return - const replacement = this.host.registerCommand(id, handler) - this.commandDisposables.set(id, replacement) + if (handler) { + const replacement = this.tryRegisterCommand(id, handler) + if (replacement) this.commandDisposables.set(id, replacement) + } + } + } + + private tryRegisterCommand(id: string, handler: (...args: unknown[]) => Promise): Disposable | undefined { + try { + return this.host.registerCommand(id, handler) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + this.log(`panel command registration skipped for ${id}: ${msg}`) + return undefined } } diff --git a/packages/kilo-vscode/src/services/attention/service.ts b/packages/kilo-vscode/src/services/attention/service.ts index c3659933ce..19bbe7bd7d 100644 --- a/packages/kilo-vscode/src/services/attention/service.ts +++ b/packages/kilo-vscode/src/services/attention/service.ts @@ -20,7 +20,6 @@ export class AttentionService implements vscode.Disposable { private readonly errored = new Set() private readonly questions = new Set() private readonly permissions = new Set() - private readonly parents = new Map() private readonly unsubscribeEvent: () => void private readonly unsubscribeState: () => void @@ -35,7 +34,6 @@ export class AttentionService implements vscode.Disposable { this.unsubscribeEvent() this.unsubscribeState() this.reset() - this.parents.clear() } private handle(event: SSEPayload) { @@ -44,26 +42,20 @@ export class AttentionService implements vscode.Disposable { return this.question(event) } if (event.type === "permission.asked" || event.type === "permission.replied") return this.permission(event) + if (event.type === "session.deleted") return this.remove(event.properties.sessionID) if (event.type === "session.status") return this.status(event) if (event.type === "session.turn.close") return this.close(event) if (event.type === "session.error") return this.error(event) } private sync(event: Sync) { - if (event.name === "session.created.1") { - this.parents.set(event.data.sessionID, event.data.info.parentID) - return - } - if (event.name === "session.updated.1") { - if (event.data.info.parentID !== undefined) { - this.parents.set(event.data.sessionID, event.data.info.parentID ?? undefined) - } - return - } if (event.name !== "session.deleted.1") return - this.parents.delete(event.data.sessionID) - this.active.delete(event.data.sessionID) - this.errored.delete(event.data.sessionID) + this.remove(event.data.sessionID) + } + + private remove(sessionID: string) { + this.active.delete(sessionID) + this.errored.delete(sessionID) } private question(event: Question) { @@ -98,7 +90,8 @@ export class AttentionService implements vscode.Disposable { if (!this.active.delete(sessionID)) return if (this.errored.delete(sessionID)) return if (event.properties.reason !== "completed") return - this.notify(this.parents.get(sessionID) ? "subagent_done" : "done") + if (event.properties.parentID !== undefined) return + this.notify("done") } private error(event: Error) { diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts index 2f1565ab4d..fead3c81f3 100644 --- a/packages/kilo-vscode/tests/accessibility.spec.ts +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -49,6 +49,66 @@ test.describe("webview accessibility ratchet", () => { }) } + test("Agent Manager keeps virtualized transcript fragments laid out", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const visibility = await page.locator("#storybook-root").evaluate((root) => { + const layout = document.createElement("div") + layout.className = "am-layout" + root.append(layout) + const names = ["assistant-message", "tool-trigger", "file", "code", "diff"] + const values = names.map((name) => { + const node = document.createElement("div") + node.dataset.component = name + layout.append(node) + return getComputedStyle(node).contentVisibility + }) + layout.remove() + return values + }) + + expect(visibility).toEqual(["visible", "visible", "visible", "visible", "visible"]) + }) + + test("Agent Manager avoids generated separator text in updating tool rows", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const content = await page.locator("#storybook-root").evaluate((root) => { + const layout = document.createElement("div") + layout.className = "am-layout" + const wrapper = document.createElement("div") + wrapper.dataset.component = "tool-part-wrapper" + wrapper.dataset.partType = "tool" + const collapsible = document.createElement("div") + collapsible.className = "tool-collapsible" + collapsible.dataset.component = "collapsible" + const title = document.createElement("span") + title.dataset.slot = "basic-tool-tool-title" + const subtitle = document.createElement("span") + subtitle.dataset.slot = "basic-tool-tool-subtitle" + collapsible.append(title, subtitle) + wrapper.append(collapsible) + layout.append(wrapper) + root.append(layout) + const value = getComputedStyle(subtitle, "::before").content + layout.remove() + return value + }) + + expect(content).toBe("none") + }) + + test("sidebar keeps transcript announcements while Agent Manager bounds them", async ({ page }) => { + await open(page, "chat--chat-view-with-messages") + await expect(page.locator(".message-list")).toHaveAttribute("role", "log") + await expect(page.locator(".message-list")).toHaveAttribute("aria-live", "polite") + + await open(page, "chat--chat-view-agent-manager-completed") + await expect(page.locator(".message-list")).not.toHaveAttribute("role") + await expect(page.locator(".message-list")).not.toHaveAttribute("aria-live") + await expect(page.locator('.sr-only[role="status"]')).toHaveAttribute("aria-live", "polite") + }) + test("Profile login exposes a keyboard-operable named control", async ({ page }) => { await open(page, "profile--not-logged-in") diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 68f994abfe..25778d6ec7 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.3.48", + "version": "7.3.49", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-vscode/tests/unit/attention.test.ts b/packages/kilo-vscode/tests/unit/attention.test.ts index 9c0acd9479..b25eb2a92c 100644 --- a/packages/kilo-vscode/tests/unit/attention.test.ts +++ b/packages/kilo-vscode/tests/unit/attention.test.ts @@ -45,23 +45,20 @@ describe("AttentionService", () => { test.service.dispose() }) - it("uses the upstream subagent completion sound", () => { + it("plays completion sounds only for parent agents", () => { const test = setup() - test.event( - event({ - type: "sync", - name: "session.created.1", - data: { sessionID: "child", info: { parentID: "parent" } }, - }), - ) - test.event( - event({ type: "sync", name: "session.updated.1", data: { sessionID: "child", info: { title: "work" } } }), - ) test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "retry" } } })) test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "idle" } } })) - test.event(event({ type: "session.turn.close", properties: { sessionID: "child", reason: "completed" } })) + test.event( + event({ + type: "session.turn.close", + properties: { sessionID: "child", parentID: "parent", reason: "completed" }, + }), + ) + test.event(event({ type: "session.status", properties: { sessionID: "parent", status: { type: "busy" } } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "parent", reason: "completed" } })) - expect(test.sounds).toEqual(["subagent_done"]) + expect(test.sounds).toEqual(["done"]) test.service.dispose() }) @@ -123,6 +120,19 @@ describe("AttentionService", () => { expect(test.sounds).toEqual([]) test.service.dispose() }) + + it("clears transitions when a session is deleted", () => { + const test = setup() + test.event(event({ type: "session.status", properties: { sessionID: "s1", status: { type: "busy" } } })) + test.event(event({ type: "session.deleted", properties: { sessionID: "s1", info: { id: "s1" } } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "s1", reason: "completed" } })) + test.event(event({ type: "session.status", properties: { sessionID: "s2", status: { type: "busy" } } })) + test.event(event({ type: "sync", name: "session.deleted.1", data: { sessionID: "s2" } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "s2", reason: "completed" } })) + + expect(test.sounds).toEqual([]) + test.service.dispose() + }) }) describe("attention defaults", () => { diff --git a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts index 788a70b61e..529e8b5565 100644 --- a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts +++ b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts @@ -22,13 +22,32 @@ describe("model preview data collection line", () => { expect(styles).toContain(".model-preview-data-line") }) - it("renders prompt training independently from the free badge", () => { - expect(selector).toMatch( - /[\s\S]*?<\/Show>\s*/, - ) - expect(preview).toMatch( - /[\s\S]*?<\/Show>\s*/, - ) + it("renders prompt training independently from the model badges", () => { + expect(selector).toContain("isDataCollectedModel(model)") + expect(preview).toContain("isDataCollectedModel(model())") + expect(agent).toContain("isDataCollectedModel(model)") + }) + + it("renders BYOK availability independently from training metadata", () => { + expect(selector).toContain("hasByok(model)") + expect(preview).toContain("hasByok(model())") + expect(agent).toContain("hasByok(model)") + expect(selector).toContain(">BYOK") + expect(preview).toContain(">BYOK") + expect(agent).toContain(">BYOK") + }) + + it("shows BYOK instead of Free when both metadata fields are set", () => { + expect(selector).toContain("isFree(model) && !hasByok(model)") + expect(preview).toContain("model().isFree && !hasByok(model())") + expect(agent).toContain("model.isFree && !hasByok(model)") + }) + + it("uses neutral colors for the main model picker BYOK badge", () => { + expect(selector).toContain("model-selector-data-badge--byok") + expect(styles).toContain(".model-selector-data-badge--byok") + expect(styles).toContain("background: var(--vscode-badge-background) !important") + expect(styles).toContain("color: var(--vscode-badge-foreground) !important") }) it("uses the book open check icon for all webview model data disclosures", () => { diff --git a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts index 4483a55488..8fbbd5e51c 100644 --- a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts @@ -8,6 +8,7 @@ import { PROVIDER_ORDER, freeDataLabel, isDataCollectedModel, + hasByok, isFree, } from "../../webview-ui/src/components/shared/model-selector-utils" @@ -119,6 +120,14 @@ describe("isDataCollectedModel", () => { }) }) +describe("hasByok", () => { + it("uses only explicit user BYOK metadata", () => { + expect(hasByok({ hasUserByokAvailable: true })).toBe(true) + expect(hasByok({ hasUserByokAvailable: false })).toBe(false) + expect(hasByok({})).toBe(false) + }) +}) + describe("buildTriggerLabel", () => { it("returns resolved model name for non-kilo provider unchanged", () => { expect(buildTriggerLabel("GPT-4o", "openai", undefined, null, false, "", true, labels)).toBe("GPT-4o") diff --git a/packages/kilo-vscode/tests/unit/session-queue.test.ts b/packages/kilo-vscode/tests/unit/session-queue.test.ts index 8e8f6934c3..77f8ae6a29 100644 --- a/packages/kilo-vscode/tests/unit/session-queue.test.ts +++ b/packages/kilo-vscode/tests/unit/session-queue.test.ts @@ -6,8 +6,10 @@ import { queuedUserMessageIDs, stableMessageTurns, visibleMessages, + visibleParts, + type RevertBoundary, } from "../../webview-ui/src/context/session-queue" -import type { Message, SessionStatusInfo } from "../../webview-ui/src/types/messages" +import type { Message, Part, SessionStatusInfo } from "../../webview-ui/src/types/messages" const base = { sessionID: "session", @@ -30,10 +32,12 @@ const assistant = (id: string, parentID: string, opts: Partial = {}): M ...opts, }) -const layout = (messages: Message[], status: SessionStatusInfo, boundary?: string) => { +const part = (id: string, messageID: string): Part => ({ id, messageID, type: "text", text: id }) + +const layout = (messages: Message[], status: SessionStatusInfo, revert?: RevertBoundary) => { const active = activeUserMessageID(messages, status) return partitionTurns( - messageTurns(messages, boundary), + messageTurns(messages, revert), new Set(active ? [active] : []), new Set(queuedUserMessageIDs(messages, status)), ) @@ -292,7 +296,7 @@ describe("partitionTurns", () => { it("does not render an active turn hidden by a revert boundary", () => { const messages = [user("message_1"), assistant("message_2", "message_1", { finish: "stop" }), user("message_3")] - const result = layout(messages, { type: "busy" }, "message_3") + const result = layout(messages, { type: "busy" }, { messageID: "message_3" }) expect(result.virtual.map((turn) => turn.user.id)).toEqual(["message_1"]) expect(result.direct).toEqual([]) @@ -394,7 +398,45 @@ describe("messageTurns", () => { assistant("message_4", "message_3"), ] - expect(messageTurns(messages, "message_3").map((turn) => turn.user.id)).toEqual(["message_1"]) + expect(messageTurns(messages, { messageID: "message_3" }).map((turn) => turn.user.id)).toEqual(["message_1"]) + }) + + it("keeps the part-boundary assistant and hides later provider errors", () => { + const messages = [ + user("message_1"), + assistant("message_2", "message_1"), + assistant("message_3", "message_1", { error: { name: "ProviderError" } }), + assistant("message_4", "message_1", { error: { name: "ProviderError" } }), + ] + const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" }) + + expect(turns).toHaveLength(1) + expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"]) + }) + + it("applies assistant boundaries by id when messages arrive out of order", () => { + const messages = [ + user("message_1"), + assistant("message_4", "message_1", { error: { name: "ProviderError" } }), + assistant("message_2", "message_1"), + ] + const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" }) + + expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"]) + }) +}) + +describe("visibleParts", () => { + it("keeps only parts before the active part boundary", () => { + const parts = [part("part_1", "message_2"), part("part_2", "message_2"), part("part_3", "message_2")] + + expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_2" })).toEqual([parts[0]]) + }) + + it("fails closed when the boundary part is unavailable", () => { + const parts = [part("part_1", "message_2")] + + expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_missing" })).toEqual([]) }) }) @@ -407,7 +449,10 @@ describe("visibleMessages", () => { assistant("message_4", "message_3"), ] - expect(visibleMessages(messages, "message_3").map((msg) => msg.id)).toEqual(["message_1", "message_2"]) + expect(visibleMessages(messages, { messageID: "message_3" }).map((msg) => msg.id)).toEqual([ + "message_1", + "message_2", + ]) }) it("keeps leading partial assistant output", () => { diff --git a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts index bb08e44b82..3025d3b0cb 100644 --- a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts @@ -1,18 +1,53 @@ /** * SessionTerminalManager tests. * - * The class is tightly coupled to VS Code terminal APIs. We use ts-morph - * static analysis to verify structural invariants that protect against - * real regressions โ€” focusing on ordering constraints and cleanup logic - * that are easy to break during refactoring. + * Structural tests use ts-morph to protect ordering and cleanup invariants. + * Command behavior tests exercise the narrow TerminalHost interface directly. */ import { describe, it, expect } from "bun:test" import path from "node:path" import { Project, SyntaxKind } from "ts-morph" +import { SessionTerminalManager, type TerminalHost } from "../../src/agent-manager/SessionTerminalManager" const ROOT = path.resolve(import.meta.dir, "../..") const FILE = path.join(ROOT, "src/agent-manager/SessionTerminalManager.ts") +const COMMAND = "workbench.action.togglePanel" + +type Handler = (...args: unknown[]) => Promise + +function runtime(run: () => Promise) { + let blocked = false + const handlers = new Map() + const host: TerminalHost = { + createTerminal() { + throw new Error("not used") + }, + activeTerminal: () => undefined, + repoPath: () => undefined, + showWarning() {}, + setContext() {}, + onTerminalClosed: () => ({ dispose() {} }), + onActiveTerminalChanged: () => ({ dispose() {} }), + registerCommand(id, handler) { + if (blocked && id === COMMAND) throw new Error(`command '${id}' already exists`) + handlers.set(id, handler) + return { + dispose() { + if (handlers.get(id) === handler) handlers.delete(id) + }, + } + }, + executeCommand() { + blocked = true + return run() + }, + } + const manager = new SessionTerminalManager(() => {}, host) + const handler = handlers.get(COMMAND) + if (!handler) throw new Error(`command '${COMMAND}' was not registered`) + return { manager, handler } +} function getClass() { const project = new Project({ compilerOptions: { allowJs: true } }) @@ -88,8 +123,35 @@ describe("SessionTerminalManager structure", () => { expect(text).toContain("this.showExistingLocal()") }) + it("panel command registration is best effort", () => { + const text = body("tryRegisterCommand") + expect(text).toContain("this.host.registerCommand") + expect(text).toContain("catch (err)") + expect(text).toContain("panel command registration skipped") + }) + it("exposes active terminal state for terminal context routing", () => { const text = body("hasActiveTerminal") expect(text).toContain("this.host.activeTerminal()") }) }) + +describe("SessionTerminalManager command restoration", () => { + it("preserves the original command result when re-registration fails", async () => { + const expected = { status: "complete" } + const state = runtime(async () => expected) + + expect(await state.handler()).toBe(expected) + state.manager.dispose() + }) + + it("preserves the original command error when re-registration fails", async () => { + const expected = new Error("panel command failed") + const state = runtime(async () => { + throw expected + }) + + await expect(state.handler()).rejects.toBe(expected) + state.manager.dispose() + }) +}) diff --git a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts new file mode 100644 index 0000000000..acb6ccafe6 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "bun:test" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "../..") + +describe("task tool index identity", () => { + it("preserves a rendered child tool row across status updates", () => { + const result = Bun.spawnSync( + [ + "bun", + "--conditions=browser", + "-e", + ` + import { Window } from "happy-dom" + + const window = new Window() + globalThis.window = window + globalThis.document = window.document + globalThis.Node = window.Node + + const { Index, createComponent, createRenderEffect } = await import("solid-js") + const { createStore } = await import("solid-js/store") + const { render } = await import("solid-js/web") + const { reconcileSessionToolParts, upsertSessionToolPart } = await import( + "./webview-ui/src/context/session-utils.ts" + ) + + const running = { + id: "tool-1", + type: "tool", + tool: "read", + state: { status: "running", input: { filePath: "src/app.ts" }, title: "Reading" }, + } + const completed = { + id: "tool-1", + type: "tool", + tool: "read", + state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" }, + } + const root = document.createElement("div") + const [store, setStore] = createStore({ tools: [running] }) + const dispose = render( + () => + createComponent(Index, { + get each() { + return store.tools + }, + children: (item) => { + const row = document.createElement("div") + createRenderEffect(() => { + row.textContent = item().state.status + }) + return row + }, + }), + root, + ) + const row = root.firstElementChild + const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) + setStore("tools", reconcileSessionToolParts(tools)) + if (root.firstElementChild !== row) throw new Error("rendered tool row was replaced") + if (row?.textContent !== "completed") throw new Error("rendered tool row was not updated") + dispose() + `, + ], + { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, + ) + + const output = result.stdout.toString() + result.stderr.toString() + expect(result.exitCode, output).toBe(0) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/transcript-rows.test.ts b/packages/kilo-vscode/tests/unit/transcript-rows.test.ts index a34628e3e8..6889aab495 100644 --- a/packages/kilo-vscode/tests/unit/transcript-rows.test.ts +++ b/packages/kilo-vscode/tests/unit/transcript-rows.test.ts @@ -84,6 +84,32 @@ describe("transcriptRows", () => { expect(rows.at(-1)).toMatchObject({ type: "error", message: a3, error: a3.error }) }) + it("hides provider errors at and after an assistant part boundary", () => { + const revert = { messageID: "message_2", partID: "part_2" } + const u1 = user("message_1") + const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } }) + const a2 = assistant("message_3", "message_1", { error: { name: "ProviderError" } }) + const parts = { message_2: [part("part_1", "message_2"), part("part_2", "message_2")] } + const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert }) + + expect(rows.map((row) => row.type)).toEqual(["user", "assistant"]) + expect(rows.filter((row) => row.type === "assistant").flatMap((row) => row.parts.map((item) => item.id))).toEqual([ + "part_1", + ]) + expect(rows.some((row) => row.message.id === "message_3")).toBe(false) + }) + + it("keeps provider errors before an assistant part boundary", () => { + const revert = { messageID: "message_3", partID: "part_2" } + const u1 = user("message_1") + const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } }) + const a2 = assistant("message_3", "message_1") + const parts = { message_3: [part("part_1", "message_3"), part("part_2", "message_3")] } + const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert }) + + expect(rows.at(-1)).toMatchObject({ type: "error", message: a1 }) + }) + it("keeps keys stable when older turns are prepended and parts are appended", () => { const u1 = user("u1") const a1 = assistant("a1", "u1") @@ -131,7 +157,7 @@ describe("transcriptRows", () => { }) const a2 = assistant("a2", "u1") const u3 = user("u3") - const turns = messageTurns([u1, a1, u2, a2, u3], "u3") + const turns = messageTurns([u1, a1, u2, a2, u3], { messageID: "u3" }) const rows = transcriptRows(turns, (id) => (id === "u2" ? (u2.parts ?? []) : [])) expect(rows.map((row) => `${row.turn}:${row.message.id}`)).toEqual(["u1:u1", "u1:a1", "u2:u2", "u2:a2"]) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx index b9d3d0aa86..71b39fed3c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx @@ -7,6 +7,7 @@ import { useLanguage } from "../src/context/language" import { KILO_GATEWAY_ID, freeDataLabel, + hasByok, isDataCollectedModel, providerSortKey, } from "../src/components/shared/model-selector-utils" @@ -115,9 +116,14 @@ export const MultiModelSelector: Component<{ } /> {model.name} - + - {freeLabel()} + + {freeLabel()} + + + BYOK + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index 5606e835c4..303661b74a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -151,8 +151,9 @@ export const WorktreeItem: Component = (props) => { = (props) => { suggestions={standaloneSuggestions} readonly={props.readonly} emptyState={props.emptyState} + announce={isSidebar()} />
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 19e8f81b96..5913ef4f36 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -63,6 +63,8 @@ interface MessageListProps { readonly?: boolean /** Optionally replace the standard welcome content while the conversation is empty. */ emptyState?: () => JSX.Element + /** Announce transcript changes as a live log. Disable for multi-session surfaces with concurrent streams. */ + announce?: boolean } export const MessageList: Component = (props) => { @@ -73,6 +75,19 @@ export const MessageList: Component = (props) => { const autoScroll = createAutoScroll({ working: () => session.status() !== "idle", }) + const [announcement, setAnnouncement] = createSignal("") + createEffect( + (prev: { sid?: string; working: boolean }) => { + const sid = session.currentSessionID() + const working = session.status() !== "idle" + if (working && (!prev.working || prev.sid !== sid)) setAnnouncement(language.t("session.status.working")) + if (!working && prev.working && prev.sid === sid) { + setAnnouncement(language.t("settings.agentBehaviour.editMode.save")) + } + return { sid, working } + }, + { sid: undefined, working: false }, + ) // Explicit output-producing actions resume auto-scroll before appending. const onResumeAutoScroll = () => autoScroll.resume() @@ -91,14 +106,14 @@ export const MessageList: Component = (props) => { const [virtualizer, setVirtualizer] = createSignal() const [layout, setLayout] = createSignal("") - const boundary = () => session.revert()?.messageID + const revert = () => session.revert() ?? undefined const turns = createMemo((prev: MessageTurn[] | undefined) => stableMessageTurns( - messageTurns(session.messages(), boundary(), (msg) => session.getParts(msg.id)), + messageTurns(session.messages(), revert(), (msg) => session.getParts(msg.id)), prev, ), ) - const isEmpty = () => turns().length === 0 && !session.loading() && !boundary() + const isEmpty = () => turns().length === 0 && !session.loading() && !revert() const activeUserID = createMemo(() => getActiveUserMessageID(session.messages(), session.statusInfo(), (msg) => session.getParts(msg.id)), @@ -115,6 +130,7 @@ export const MessageList: Component = (props) => { queued: queuedIDs(), live: new Set(active ? [active] : []), hidden: session.isErrorHidden, + revert: revert(), }, prev, ) @@ -260,13 +276,25 @@ export const MessageList: Component = (props) => { return (
+ +
+ {announcement()} +
+
-
+
@@ -326,7 +354,7 @@ export const MessageList: Component = (props) => {
- + {(row) => } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx index 62162fda12..a05380b245 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx @@ -7,7 +7,7 @@ * Call registerExpandedTaskTool() once at app startup to activate. */ -import { Component, createEffect, createMemo, createSignal, For, Show, onCleanup } from "solid-js" +import { Component, createEffect, createMemo, createSignal, Index, Show, onCleanup } from "solid-js" import { ToolRegistry, ToolProps, getToolInfo } from "@kilocode/kilo-ui/message-part" import { BasicTool, initialOpen } from "@kilocode/kilo-ui/basic-tool" import { Icon } from "@kilocode/kilo-ui/icon" @@ -141,15 +141,13 @@ const TaskToolRenderer: Component = (props) => {
{(text) => } - + {(item) => { - const info = createMemo(() => getToolInfo(item.tool, item.state?.input)) + const info = createMemo(() => getToolInfo(item().tool, item().state?.input)) const subtitle = createMemo(() => { if (info().subtitle) return info().subtitle - const state = item.state as { status: string; title?: string } - if (state.status === "completed" || state.status === "running") { - return state.title - } + const state = item().state as { status: string; title?: string } + if (state.status === "completed" || state.status === "running") return state.title return undefined }) return ( @@ -162,7 +160,7 @@ const TaskToolRenderer: Component = (props) => {
) }} - +
diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx index ae3589f86d..485d3b8003 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx @@ -5,7 +5,7 @@ import { Markdown } from "@kilocode/kilo-ui/markdown" import { Icon } from "@kilocode/kilo-ui/icon" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../context/language" -import { freeDataLabel, isDataCollectedModel, sanitizeName } from "./model-selector-utils" +import { freeDataLabel, hasByok, isDataCollectedModel, sanitizeName } from "./model-selector-utils" import { avgPrice, fmtAttemptCost, fmtCachedPrice, fmtPrice, fmtTerminalBenchScore } from "./model-preview-utils" interface Props { @@ -91,11 +91,14 @@ export const ModelPreview: Component = (props) => { })()}
- + - + {freeLabel()} + + BYOK + diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index df7ff77f08..e856d5c1f6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -38,6 +38,7 @@ import { providerSortKey, isFree, isDataCollectedModel, + hasByok, freeDataLabel, buildTriggerLabel, sanitizeName, @@ -936,13 +937,18 @@ export const ModelSelectorBase: Component = (props) => { ) })()} - + - + {freeLabel()} + + + BYOK + + diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts index f5a183f184..5c75b79782 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts @@ -26,6 +26,10 @@ export function isDataCollectedModel(model: Pick): boolean { + return model.hasUserByokAvailable === true +} + export function freeDataLabel(_free: string, data: string): string { return data } diff --git a/packages/kilo-vscode/webview-ui/src/context/notifications.tsx b/packages/kilo-vscode/webview-ui/src/context/notifications.tsx index 67deb6f99c..8c722585e6 100644 --- a/packages/kilo-vscode/webview-ui/src/context/notifications.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/notifications.tsx @@ -11,16 +11,6 @@ import { import { useVSCode } from "./vscode" import type { KilocodeNotification, ExtensionMessage } from "../types/messages" -// Static notifications always shown unconditionally (not fetched from API) -const STATIC_NOTIFICATIONS: KilocodeNotification[] = [ - { - id: "star-giveaway-june-2026", - title: "GitHub Star Giveaway", - message: "We're giving away $500 of AI Credits when we reach 25,000 stars on GitHub. Support us:", - action: { actionText: "github.com/Kilo-Org/kilocode", actionURL: "https://github.com/Kilo-Org/kilocode/" }, - }, -] - interface NotificationsContextValue { notifications: Accessor filteredNotifications: Accessor @@ -63,8 +53,7 @@ export const NotificationsProvider: ParentComponent = (props) => { const filteredNotifications = createMemo(() => { const dismissed = dismissedIds() - const all = [...STATIC_NOTIFICATIONS, ...notifications()] - return all.filter((n) => !dismissed.includes(n.id)) + return notifications().filter((n) => !dismissed.includes(n.id)) }) const dismiss = (id: string) => { diff --git a/packages/kilo-vscode/webview-ui/src/context/session-queue.ts b/packages/kilo-vscode/webview-ui/src/context/session-queue.ts index 066df58329..59b7833551 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-queue.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-queue.ts @@ -1,4 +1,6 @@ -import type { Message, SessionStatusInfo } from "../types/messages" +import type { Message, Part, SessionInfo, SessionStatusInfo } from "../types/messages" + +export type RevertBoundary = Pick, "messageID" | "partID"> export interface MessageTurn { id: string @@ -55,24 +57,37 @@ function target(messages: Message[], index: number, id: string, parts?: (msg: Me return id } +function visibleMessage(id: string, revert?: RevertBoundary) { + if (!revert || id < revert.messageID) return true + return id === revert.messageID && !!revert.partID +} + +export function visibleParts(id: string, parts: Part[], revert?: RevertBoundary) { + if (!revert || id < revert.messageID) return parts + if (id !== revert.messageID || !revert.partID) return [] + const idx = parts.findIndex((part) => part.id === revert.partID) + return idx < 0 ? [] : parts.slice(0, idx) +} + export function messageTurns( messages: Message[], - boundary?: string, + revert?: RevertBoundary, parts?: (msg: Message) => Message["parts"], ): MessageTurn[] { const result: MessageTurn[] = [] const lead: Message[] = [] const by = new Map() + const projected = (msg: Message) => visibleParts(msg.id, parts?.(msg) ?? msg.parts ?? [], revert) let compact: { turn: MessageTurn; index: number } | undefined for (const msg of messages) { + if (!visibleMessage(msg.id, revert)) continue if (msg.role === "user") { - if (boundary && msg.id >= boundary) break const turn = { id: msg.id, user: msg, assistant: [] } const item = { turn, index: result.length } result.push(turn) by.set(msg.id, item) - if (isCompact(msg, parts)) compact = item + if (isCompact(msg, projected)) compact = item continue } @@ -105,10 +120,10 @@ export function messageTurns( export function visibleMessages( messages: Message[], - boundary?: string, + revert?: RevertBoundary, parts?: (msg: Message) => Message["parts"], ): Message[] { - return messageTurns(messages, boundary, parts).flatMap((turn) => + return messageTurns(messages, revert, parts).flatMap((turn) => turn.partial ? turn.assistant : [turn.user, ...turn.assistant], ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index 9e7798119a..ae448a3261 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -1,3 +1,4 @@ +import { reconcile } from "solid-js/store" import type { Message, Part, ToolPart } from "../types/messages" export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..." @@ -85,6 +86,10 @@ export function buildSessionToolParts( return tools } +export function reconcileSessionToolParts(tools: ToolPart[]) { + return reconcile(tools, { key: "id" }) +} + export function upsertSessionToolPart( current: ToolPart[], part: Part, diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 01788f5d66..235b3ea1d3 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -57,6 +57,7 @@ import { buildCostBreakdown, buildSessionToolParts, childID, + reconcileSessionToolParts, removeSessionToolPart, removeSessionToolPartsForMessage, upsertSessionToolPart, @@ -1290,12 +1291,16 @@ export const SessionProvider: ParentComponent = (props) => { return true } + function setTools(sessionID: string, tools: ToolPart[]) { + setStore("toolParts", sessionID, reconcileSessionToolParts(tools)) + } + function rebuildToolParts(sessionID: string, messages: Message[], parts?: Record) { const tools = buildSessionToolParts( messages, (msg) => parts?.[msg.id] ?? store.parts[msg.id] ?? stash.peek(msg.id) ?? msg.parts, ) - setStore("toolParts", sessionID, tools) + setTools(sessionID, tools) } function messageParts(messages: Message[]): Record { @@ -1310,16 +1315,17 @@ export const SessionProvider: ParentComponent = (props) => { const sid = sessionID ?? part.sessionID if (!sid) return if (part.type !== "tool") return - setStore("toolParts", sid, (tools = []) => upsertSessionToolPart(tools, part, { id: messageID, sessionID: sid })) + const tools = upsertSessionToolPart(store.toolParts[sid] ?? [], part, { id: messageID, sessionID: sid }) + setTools(sid, tools) } function dropToolPart(sessionID: string | undefined, partID: string) { if (!sessionID) return - setStore("toolParts", sessionID, (tools = []) => removeSessionToolPart(tools, partID)) + setTools(sessionID, removeSessionToolPart(store.toolParts[sessionID] ?? [], partID)) } function dropMessageTools(sessionID: string, messageID: string) { - setStore("toolParts", sessionID, (tools = []) => removeSessionToolPartsForMessage(tools, messageID)) + setTools(sessionID, removeSessionToolPartsForMessage(store.toolParts[sessionID] ?? [], messageID)) } function handleMessagesLoaded( @@ -2550,8 +2556,10 @@ export const SessionProvider: ParentComponent = (props) => { const userMessages = createMemo(() => messages().filter((m) => m.role === "user")) function visible(sessionID: string) { - return filterVisibleMessages(store.messages[sessionID] ?? [], store.sessions[sessionID]?.revert?.messageID, (msg) => - getParts(msg.id), + return filterVisibleMessages( + store.messages[sessionID] ?? [], + store.sessions[sessionID]?.revert ?? undefined, + (msg) => getParts(msg.id), ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts b/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts index c6445d4c3c..89ac134335 100644 --- a/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts +++ b/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts @@ -1,5 +1,5 @@ import type { Message, Part } from "../types/messages" -import type { MessageTurn } from "./session-queue" +import { visibleParts, type MessageTurn, type RevertBoundary } from "./session-queue" interface TranscriptMeta { turn: string @@ -46,6 +46,7 @@ export interface TranscriptOptions { queued?: ReadonlySet live?: ReadonlySet hidden?: (id: string) => boolean + revert?: RevertBoundary } export interface TranscriptPartition { @@ -127,6 +128,8 @@ export function transcriptRows( ): TranscriptRow[] { const size = Math.max(1, Math.floor(opts.size ?? 8)) const rows: TranscriptRow[] = [] + const parts = (id: string) => visibleParts(id, getParts(id), opts.revert) + const terminal = (msg: Message) => !(opts.revert?.partID && msg.id === opts.revert.messageID) for (const turn of turns) { const meta = { @@ -135,7 +138,7 @@ export function transcriptRows( queued: opts.queued?.has(turn.id) === true, live: opts.live?.has(turn.id) === true, } - const copied = copy(turn.assistant, getParts) + const copied = copy(turn.assistant, parts) if (!turn.partial) { rows.push({ @@ -143,27 +146,27 @@ export function transcriptRows( type: "user", key: `${turn.id}:user`, message: turn.user, - parts: getParts(turn.user.id), - interrupted: turn.assistant.some((msg) => msg.error?.name === "MessageAbortedError"), + parts: parts(turn.user.id), + interrupted: turn.assistant.some((msg) => terminal(msg) && msg.error?.name === "MessageAbortedError"), answered: turn.assistant.length > 0, }) } for (const msg of turn.assistant) { - const parts = getParts(msg.id) - if (parts.length === 0) { + const visible = parts(msg.id) + if (visible.length === 0) { rows.push({ ...meta, type: "assistant", key: `${turn.id}:assistant:${msg.id}:empty`, message: msg, - parts, + parts: visible, copy: copied, }) continue } - for (let start = 0; start < parts.length; start += size) { - const chunk = parts.slice(start, start + size) + for (let start = 0; start < visible.length; start += size) { + const chunk = visible.slice(start, start + size) rows.push({ ...meta, type: "assistant", @@ -181,7 +184,7 @@ export function transcriptRows( } const failed = turn.assistant.find( - (msg) => msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true, + (msg) => terminal(msg) && msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true, ) if (failed?.error) { rows.push({ ...meta, type: "error", key: `${turn.id}:error:${failed.id}`, message: failed, error: failed.error }) diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index fbea166aff..dcb17a5a6a 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -18,7 +18,7 @@ min-height: 0; overflow: hidden; container: chat / inline-size; - --chat-readable-width: 88ch; + --chat-readable-width: 98ch; --chat-gutter: clamp(16px, 4cqi, 48px); --chat-scrollbar-width: 10px; } diff --git a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css index 7140c77dd8..387d1bd395 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css +++ b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css @@ -437,6 +437,11 @@ opacity: 0.8; } +.model-selector-data-badge--byok [data-component="tag"] { + background: var(--vscode-badge-background) !important; + color: var(--vscode-badge-foreground) !important; +} + .model-selector-trigger-free-data { display: inline-flex; align-items: center; diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts index 67c865be0b..9726f263f3 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts @@ -19,6 +19,7 @@ export interface ProviderModel { recommendedIndex?: number isFree?: boolean mayTrainOnYourPrompts?: boolean + hasUserByokAvailable?: boolean terminalBench?: { overallScore: number avgAttemptCostUsd: number diff --git a/packages/kilo-web-ui/package.json b/packages/kilo-web-ui/package.json index 2c3a0e9a68..9a0e491bfb 100644 --- a/packages/kilo-web-ui/package.json +++ b/packages/kilo-web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-web-ui", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "exports": { diff --git a/packages/llm/package.json b/packages/llm/package.json index 4fbb7aa8c9..6f7a4b58d8 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/CHANGELOG.md b/packages/opencode/CHANGELOG.md index 7335b645ac..1399f55759 100644 --- a/packages/opencode/CHANGELOG.md +++ b/packages/opencode/CHANGELOG.md @@ -1,5 +1,7 @@ # @kilocode/cli +## 7.3.49 + ## 7.3.48 ### Minor Changes diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0d35b24c9c..3db70fbaf7 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@kilocode/cli", "type": "module", "license": "MIT", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 2a3378ed96..b64c8008be 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -64,9 +64,12 @@ export function DialogModel(props: { providerID?: string }) { }) const footer = (providerID: string, model: Model) => { - if (providerID === "kilo" && FreeModelDisclosure.collectsData(model)) return FreeModelDisclosure.label - if (model.cost?.input === 0 && providerID === "opencode") return "Free" - return undefined + const labels = [ + providerID === "kilo" && FreeModelDisclosure.hasByok(model) ? FreeModelDisclosure.byok : undefined, + providerID === "kilo" && FreeModelDisclosure.collectsData(model) ? FreeModelDisclosure.label : undefined, + model.cost?.input === 0 && providerID === "opencode" ? "Free" : undefined, + ].filter((label) => label !== undefined) + return labels.length > 0 ? labels.join(" ยท ") : undefined } // kilocode_change end diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts index 11ce2b6bc3..9afaacaf4b 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts @@ -69,8 +69,8 @@ const tui: TuiPlugin = async (api) => { if (!active.delete(sessionID)) return if (errored.delete(sessionID)) return if (event.properties.reason !== "completed") return - const session = api.state.session.get(sessionID) - notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done") + if (event.properties.parentID !== undefined) return + notify(api, sessionID, "Session done", "done") }) // kilocode_change end diff --git a/packages/opencode/src/kilocode/components/free-model-disclosure.ts b/packages/opencode/src/kilocode/components/free-model-disclosure.ts index f28ce09934..b9cb56ccb4 100644 --- a/packages/opencode/src/kilocode/components/free-model-disclosure.ts +++ b/packages/opencode/src/kilocode/components/free-model-disclosure.ts @@ -1,7 +1,11 @@ export const FreeModelDisclosure = { label: "May train", panel: "Data may be used for training", + byok: "BYOK", collectsData(model: { mayTrainOnYourPrompts?: boolean }): boolean { return model.mayTrainOnYourPrompts === true }, + hasByok(model: { hasUserByokAvailable?: boolean }): boolean { + return model.hasUserByokAvailable === true + }, } as const diff --git a/packages/opencode/src/kilocode/components/model-info-panel.tsx b/packages/opencode/src/kilocode/components/model-info-panel.tsx index e726d717ad..e7fa189349 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel.tsx +++ b/packages/opencode/src/kilocode/components/model-info-panel.tsx @@ -75,6 +75,11 @@ export function ModelInfoPanel(props: Props) { {props.provider ?? m().providerID ?? ""} + + + {FreeModelDisclosure.byok} + + {FreeModelDisclosure.panel} diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index cb35d737ac..efcf0a0f6e 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -36,6 +36,7 @@ export const KILO_MODEL_SCHEMA_EXTENSIONS = { prompt: Schema.optional(Schema.Literals(PROMPTS)), isFree: Schema.optional(Schema.Boolean), mayTrainOnYourPrompts: Schema.optional(Schema.Boolean), + hasUserByokAvailable: Schema.optional(Schema.Boolean), terminalBench: optionalOmitUndefined( Schema.Struct({ overallScore: Schema.Finite, @@ -56,6 +57,7 @@ export function patchModelsDevModel(providerID: string, source: any) { prompt: source.prompt, isFree: source.isFree, mayTrainOnYourPrompts: source.mayTrainOnYourPrompts, + hasUserByokAvailable: source.hasUserByokAvailable, terminalBench: source.terminalBench, ai_sdk_provider: source.ai_sdk_provider, options: source.options ?? {}, @@ -72,6 +74,7 @@ export function patchConfigModel(cfg: any, existing: any) { prompt: cfg.prompt ?? existing?.prompt, isFree: cfg.isFree ?? existing?.isFree, mayTrainOnYourPrompts: cfg.mayTrainOnYourPrompts ?? existing?.mayTrainOnYourPrompts, + hasUserByokAvailable: cfg.hasUserByokAvailable ?? existing?.hasUserByokAvailable, terminalBench: existing?.terminalBench, ai_sdk_provider: cfg.ai_sdk_provider ?? existing?.ai_sdk_provider, variants: cfg.variants @@ -115,6 +118,11 @@ function useLanguageModel(sdk: any) { return sdk.responses === undefined && sdk.chat === undefined } +export function patchKiloProviderPrivacy(provider: { options?: Record } | undefined, config: any) { + if (!provider || config.hide_prompt_training_models !== true) return + provider.options = { ...provider.options, dataCollection: "deny" } +} + export function kiloCustomLoaders(dep: CustomDep): Record { return { "github-copilot-enterprise": () => @@ -129,10 +137,11 @@ export function kiloCustomLoaders(dep: CustomDep): Record kilo: Effect.fnUntraced(function* (input: any) { const env = yield* dep.env() + const config = yield* dep.config() const hasKey = yield* Effect.gen(function* () { if (input.env.some((item: string) => env[item])) return true if (yield* dep.auth(input.id)) return true - if ((yield* dep.config()).provider?.["kilo"]?.options?.apiKey) return true + if (config.provider?.["kilo"]?.options?.apiKey) return true return false }) @@ -140,6 +149,9 @@ export function kiloCustomLoaders(dep: CustomDep): Record if (env.KILO_ORG_ID) { options.kilocodeOrganizationId = env.KILO_ORG_ID } + if (config.hide_prompt_training_models === true) { + options.dataCollection = "deny" + } if (!hasKey) { options.apiKey = "anonymous" } diff --git a/packages/opencode/src/kilocode/session/index.ts b/packages/opencode/src/kilocode/session/index.ts index 708592fe0d..19f824ff3f 100644 --- a/packages/opencode/src/kilocode/session/index.ts +++ b/packages/opencode/src/kilocode/session/index.ts @@ -39,6 +39,7 @@ export namespace KiloSession { "session.turn.close", Schema.Struct({ sessionID: SessionID, + parentID: Schema.optional(SessionID), reason: CloseReasonSchema, }), ), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 4b4062bcad..562c522353 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -36,6 +36,7 @@ import { patchModelsDevModel as patchKiloModel, patchConfigModel as patchKiloConfigModel, patchCustomLoaderResult, + patchKiloProviderPrivacy, kiloSmallModelPriority, buildTimeoutSignal, REQUEST_TIMEOUT_MS, @@ -1516,6 +1517,7 @@ export const layer = Layer.effect( if (provider.options) partial.options = provider.options mergeProvider(providerID, partial) } + patchKiloProviderPrivacy(providers[ProviderID.make("kilo")], cfg) // kilocode_change const gitlab = ProviderID.make("gitlab") if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 648f707d45..b27814000d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1763,6 +1763,7 @@ export const layer = Layer.effect( "SessionPrompt.loop", )(function* (input: LoopInput) { // kilocode_change start + const session = yield* sessions.get(input.sessionID) yield* KiloSessionPrompt.recoverDanglingAssistant({ sessionID: input.sessionID, status, sessions }) yield* KiloSessionPrompt.recoverProviderFinishError({ sessionID: input.sessionID, status, sessions }) yield* bus.publish(KiloSession.Event.TurnOpen, { sessionID: input.sessionID }) @@ -1775,6 +1776,7 @@ export const layer = Layer.effect( Effect.fnUntraced(function* (exit) { yield* bus.publish(KiloSession.Event.TurnClose, { sessionID: input.sessionID, + parentID: session.parentID, reason: KiloSessionPrompt.resolveCloseReason({ sessionID: input.sessionID, closeReasons, diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 0c0e33da2f..def804a109 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -152,6 +152,12 @@ export const layer = Layer.effect( partID: part.id, }) } + // kilocode_change start - clear a reverted provider error from the retained assistant message + if (target.info.role === "assistant" && target.info.error) { + delete target.info.error + yield* sessions.updateMessage(target.info) + } + // kilocode_change end } } yield* sessions.clearRevert(sessionID) diff --git a/packages/opencode/test/cli/cmd/tui/notifications.test.ts b/packages/opencode/test/cli/cmd/tui/notifications.test.ts index be5d34c9c8..432f9e1711 100644 --- a/packages/opencode/test/cli/cmd/tui/notifications.test.ts +++ b/packages/opencode/test/cli/cmd/tui/notifications.test.ts @@ -193,7 +193,7 @@ describe("internal notifications TUI plugin", () => { expect(harness.notifications).toEqual([]) }) - test("uses sound-only notifications and subagent_done sound for subagent sessions", async () => { + test("keeps subagent request notifications but suppresses completion attention", async () => { const harness = await setup() harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1", "subagent") }) @@ -210,7 +210,7 @@ describe("internal notifications TUI plugin", () => { harness.emit({ id: "event-4", type: "session.turn.close", - properties: { sessionID: "subagent", reason: "completed" }, + properties: { sessionID: "subagent", parentID: "session", reason: "completed" }, }) expect(harness.notifications).toEqual([ @@ -220,12 +220,6 @@ describe("internal notifications TUI plugin", () => { notification: false, sound: { name: "question", when: "always" }, }, - { - title: "Subagent session", - message: "Session done", - notification: false, - sound: { name: "subagent_done", when: "always" }, - }, ]) }) diff --git a/packages/opencode/test/kilocode/free-model-disclosure.test.ts b/packages/opencode/test/kilocode/free-model-disclosure.test.ts index f1825c4148..7105ccdb6e 100644 --- a/packages/opencode/test/kilocode/free-model-disclosure.test.ts +++ b/packages/opencode/test/kilocode/free-model-disclosure.test.ts @@ -12,4 +12,11 @@ describe("FreeModelDisclosure", () => { expect(FreeModelDisclosure.collectsData({ mayTrainOnYourPrompts: false })).toBe(false) expect(FreeModelDisclosure.collectsData({})).toBe(false) }) + + test("uses only explicit user BYOK metadata", () => { + expect(FreeModelDisclosure.byok).toBe("BYOK") + expect(FreeModelDisclosure.hasByok({ hasUserByokAvailable: true })).toBe(true) + expect(FreeModelDisclosure.hasByok({ hasUserByokAvailable: false })).toBe(false) + expect(FreeModelDisclosure.hasByok({})).toBe(false) + }) }) diff --git a/packages/opencode/test/kilocode/kilo-loader-auth.test.ts b/packages/opencode/test/kilocode/kilo-loader-auth.test.ts index 30b8fd0103..c32c8dfde1 100644 --- a/packages/opencode/test/kilocode/kilo-loader-auth.test.ts +++ b/packages/opencode/test/kilocode/kilo-loader-auth.test.ts @@ -7,7 +7,7 @@ import { ModelsDev } from "../../src/provider/models" import * as CoreModels from "@opencode-ai/core/models-dev" import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" -import { kiloCustomLoaders } from "../../src/kilocode/provider/provider" +import { kiloCustomLoaders, patchKiloProviderPrivacy } from "../../src/kilocode/provider/provider" import { Auth } from "../../src/auth" import { ModelCache } from "../../src/provider/model-cache" import { Provider } from "../../src/provider/provider" @@ -165,6 +165,21 @@ it.effect("enables a paid catalog when config apiKey is present", () => }), ) +it.effect("denies provider data collection when prompt-training models are hidden", () => + Effect.gen(function* () { + const result = yield* load({ config: { hide_prompt_training_models: true } }) + expect(result.options).toEqual({ apiKey: "anonymous", dataCollection: "deny" }) + }), +) + +it.effect("keeps data collection denied after configured options are applied", () => + Effect.sync(() => { + const provider = { options: { dataCollection: "allow", baseURL: "https://api.kilo.ai" } } + patchKiloProviderPrivacy(provider, { hide_prompt_training_models: true }) + expect(provider.options).toEqual({ dataCollection: "deny", baseURL: "https://api.kilo.ai" }) + }), +) + it.effect("enables a paid catalog when auth exists", () => Effect.gen(function* () { const result = yield* load({ auth: { type: "api", key: "test-key" } }) diff --git a/packages/opencode/test/kilocode/session/platform-attribution.test.ts b/packages/opencode/test/kilocode/session/platform-attribution.test.ts index 5632e0ffd7..977fc09e49 100644 --- a/packages/opencode/test/kilocode/session/platform-attribution.test.ts +++ b/packages/opencode/test/kilocode/session/platform-attribution.test.ts @@ -1,21 +1,61 @@ import { describe, expect, test } from "bun:test" import path from "path" +import { Effect } from "effect" import * as Log from "@opencode-ai/core/util/log" +import { Bus } from "@/bus" import { Session as SessionNs } from "@/session/session" -import { AppRuntime } from "../../../src/effect/app-runtime" +import { SessionPrompt } from "@/session/prompt" +import { AppRuntime, type AppServices } from "../../../src/effect/app-runtime" import { KiloSession } from "../../../src/kilocode/session" import { provideTestInstance } from "../../fixture/fixture" -import type { SessionID } from "../../../src/session/schema" +import { MessageID, type SessionID } from "../../../src/session/schema" +import { ModelID, ProviderID } from "../../../src/provider/schema" const projectRoot = path.join(__dirname, "../../..") void Log.init({ print: false }) +function run(effect: Effect.Effect) { + return AppRuntime.runPromise(effect) +} + function create(input?: SessionNs.CreateInput) { - return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input))) + return run(SessionNs.Service.use((svc) => svc.create(input))) +} + +function seed(id: SessionID) { + return run( + SessionNs.Service.use((svc) => + Effect.gen(function* () { + const user = yield* svc.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: id, + agent: "build", + model: { modelID: ModelID.make("test-model"), providerID: ProviderID.make("test") }, + time: { created: Date.now() }, + }) + yield* svc.updateMessage({ + id: MessageID.ascending(), + role: "assistant", + parentID: user.id, + sessionID: id, + mode: "build", + agent: "build", + cost: 0, + path: { cwd: projectRoot, root: projectRoot }, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ModelID.make("test-model"), + providerID: ProviderID.make("test"), + time: { created: Date.now() }, + finish: "stop", + }) + }), + ), + ) } function remove(id: SessionID) { - return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.remove(id))) + return run(SessionNs.Service.use((svc) => svc.remove(id))) } describe("session platform attribution", () => { @@ -55,4 +95,31 @@ describe("session platform attribution", () => { }, }) }) + + test("turn close events include persisted parent lineage", async () => { + await provideTestInstance({ + directory: projectRoot, + fn: async () => { + const root = await create({}) + const child = await create({ parentID: root.id, title: "child" }) + await seed(child.id) + KiloSession.clearPlatformOverride(child.id) + expect(KiloSession.resolveParent(child.id)).toBeUndefined() + + const closed = Promise.withResolvers() + const unsubscribe = await run( + Bus.Service.use((bus) => + bus.subscribeCallback(KiloSession.Event.TurnClose, (event) => { + if (event.properties.sessionID === child.id) closed.resolve(event.properties.parentID) + }), + ), + ) + + await run(SessionPrompt.Service.use((prompt) => prompt.loop({ sessionID: child.id }))) + expect(await closed.promise).toBe(root.id) + unsubscribe() + await remove(root.id) + }, + }) + }) }) diff --git a/packages/opencode/test/kilocode/session/revert.test.ts b/packages/opencode/test/kilocode/session/revert.test.ts new file mode 100644 index 0000000000..91f2d008ee --- /dev/null +++ b/packages/opencode/test/kilocode/session/revert.test.ts @@ -0,0 +1,85 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { ModelID, ProviderID } from "@/provider/schema" +import { MessageV2 } from "@/session/message-v2" +import { SessionRevert } from "@/session/revert" +import { MessageID, PartID } from "@/session/schema" +import { Session } from "@/session/session" +import { Snapshot } from "@/snapshot" +import { provideTmpdirInstance } from "../../fixture/fixture" +import { testEffect } from "../../lib/effect" + +const env = Layer.mergeAll( + Session.defaultLayer, + SessionRevert.defaultLayer, + Snapshot.defaultLayer, + CrossSpawnSpawner.defaultLayer, +) +const it = testEffect(env) + +describe("partial assistant revert", () => { + it.live( + "clears provider errors when the revert becomes permanent", + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const sessions = yield* Session.Service + const revert = yield* SessionRevert.Service + const session = yield* sessions.create({}) + const providerID = ProviderID.make("test") + const user = yield* sessions.updateMessage({ + id: MessageID.ascending(), + sessionID: session.id, + role: "user", + agent: "default", + model: { providerID, modelID: ModelID.make("test") }, + time: { created: Date.now() }, + }) + const assistant = yield* sessions.updateMessage({ + id: MessageID.ascending(), + sessionID: session.id, + role: "assistant", + parentID: user.id, + mode: "default", + agent: "default", + path: { cwd: dir, root: dir }, + cost: 1, + tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ModelID.make("test"), + providerID, + time: { created: Date.now(), completed: Date.now() }, + finish: "error", + error: MessageV2.fromError(new Error("Provider returned error"), { providerID }), + }) + const kept = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: assistant.id, + sessionID: session.id, + type: "text", + text: "keep", + }) + const boundary = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: assistant.id, + sessionID: session.id, + type: "text", + text: "remove", + }) + + yield* sessions.setRevert({ + sessionID: session.id, + revert: { messageID: assistant.id, partID: boundary.id }, + summary: { additions: 0, deletions: 0, files: 0 }, + }) + yield* revert.cleanup(yield* sessions.get(session.id)) + + const messages = yield* sessions.messages({ sessionID: session.id }) + const result = messages.find((message) => message.info.id === assistant.id) + expect(result?.parts.map((part) => part.id)).toEqual([kept.id]) + expect(result?.info).not.toHaveProperty("error") + }), + { git: true }, + ), + ) +}) diff --git a/packages/plugin-atomic-chat/package.json b/packages/plugin-atomic-chat/package.json index 88bcfafb44..e93b407dc3 100644 --- a/packages/plugin-atomic-chat/package.json +++ b/packages/plugin-atomic-chat/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/plugin-atomic-chat", - "version": "7.3.48", + "version": "7.3.49", "description": "Kilo Code plugin for Atomic Chat: auto-detection and dynamic model discovery (OpenAI-compatible local API)", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 65d77c49b3..ad4dd17f18 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.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/script/package.json b/packages/script/package.json index 77a7f59988..ad86d33e46 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -12,6 +12,6 @@ "exports": { ".": "./src/index.ts" }, - "version": "7.3.48", + "version": "7.3.49", "peerDependencies": {} } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 76158f4d12..fc6a0b1e6e 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.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9c6f7f898c..0c97533dc9 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1620,6 +1620,7 @@ export type Model = { prompt?: "codex" | "gemini" | "beast" | "anthropic" | "trinity" | "anthropic_without_todo" | "ling" | "gpt55" isFree?: boolean mayTrainOnYourPrompts?: boolean + hasUserByokAvailable?: boolean terminalBench?: { overallScore: number avgAttemptCostUsd: number @@ -3158,6 +3159,7 @@ export type EventSessionTurnClose = { type: "session.turn.close" properties: { sessionID: string + parentID?: string reason: "completed" | "error" | "interrupted" } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 915815243b..a0f3ffca47 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -16575,6 +16575,12 @@ { "$ref": "#/components/schemas/EventSessionIdle" }, + { + "$ref": "#/components/schemas/EventInstallationUpdated" + }, + { + "$ref": "#/components/schemas/EventInstallationUpdate-available" + }, { "$ref": "#/components/schemas/EventSuggestionShown" }, @@ -16626,12 +16632,6 @@ { "$ref": "#/components/schemas/EventPtyDeleted" }, - { - "$ref": "#/components/schemas/EventInstallationUpdated" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdate-available" - }, { "$ref": "#/components/schemas/EventMessageUpdated" }, @@ -19345,6 +19345,12 @@ { "$ref": "#/components/schemas/EventSessionIdle" }, + { + "$ref": "#/components/schemas/EventInstallationUpdated" + }, + { + "$ref": "#/components/schemas/EventInstallationUpdate-available" + }, { "$ref": "#/components/schemas/EventSuggestionShown" }, @@ -19396,12 +19402,6 @@ { "$ref": "#/components/schemas/EventPtyDeleted" }, - { - "$ref": "#/components/schemas/EventInstallationUpdated" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdate-available" - }, { "$ref": "#/components/schemas/EventMessageUpdated" }, @@ -21282,6 +21282,9 @@ "mayTrainOnYourPrompts": { "type": "boolean" }, + "hasUserByokAvailable": { + "type": "boolean" + }, "terminalBench": { "type": "object", "properties": { @@ -26186,6 +26189,10 @@ "type": "string", "pattern": "^ses" }, + "parentID": { + "type": "string", + "pattern": "^ses" + }, "reason": { "type": "string", "enum": ["completed", "error", "interrupted"] @@ -26362,6 +26369,54 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventInstallationUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["installation.updated"] + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventInstallationUpdate-available": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["installation.update-available"] + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventSuggestionShown": { "type": "object", "properties": { @@ -26816,54 +26871,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventInstallationUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["installation.updated"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventInstallationUpdate-available": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["installation.update-available"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventMessageUpdated": { "type": "object", "properties": { diff --git a/packages/storybook/package.json b/packages/storybook/package.json index ce24afe4c7..7ece74c2c8 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -26,7 +26,7 @@ "typescript": "catalog:", "vite": "catalog:" }, - "version": "7.3.48", + "version": "7.3.49", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/ui/package.json b/packages/ui/package.json index 91a96f8842..6c39942f15 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "exports": { diff --git a/script/check-opencode-annotations.ts b/script/check-opencode-annotations.ts index be65e4e60b..b5c4e456ab 100644 --- a/script/check-opencode-annotations.ts +++ b/script/check-opencode-annotations.ts @@ -50,6 +50,7 @@ const EXEMPT_SCOPES = [ "script/check-opencode-annotations.ts", "packages/script/tests/check-opencode-annotations.test.ts", ".github/workflows/check-opencode-annotations.yml", + ".github/workflows/watch-opencode-releases.yml", ] const args = process.argv.slice(2) diff --git a/script/check-opencode-promise-facades.ts b/script/check-opencode-promise-facades.ts index dc8042c761..4f81f70679 100644 --- a/script/check-opencode-promise-facades.ts +++ b/script/check-opencode-promise-facades.ts @@ -32,7 +32,7 @@ const testAllow: Record = { "kilocode/config-resilience.test.ts": { count: 4, reason: "existing runtime integration test" }, "kilocode/config-validation.test.ts": { count: 2, reason: "existing runtime integration test" }, "kilocode/plan-followup.test.ts": { count: 4, reason: "existing runtime integration test" }, - "kilocode/session/platform-attribution.test.ts": { count: 3, reason: "existing runtime integration test" }, + "kilocode/session/platform-attribution.test.ts": { count: 2, reason: "existing runtime integration test" }, "kilocode/session-prompt-queue.test.ts": { count: 5, reason: "prompt queue legacy instance bridge regression" }, "server/experimental-session-list.test.ts": { count: 2, reason: "Kilo session list integration test" }, "kilocode/server/listener-runtime.test.ts": { count: 3, reason: "listener and AppRuntime integration test" }, diff --git a/script/upstream/package.json b/script/upstream/package.json index 6520100029..7435f5721d 100644 --- a/script/upstream/package.json +++ b/script/upstream/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/upstream-merge", - "version": "7.3.48", + "version": "7.3.49", "private": true, "type": "module", "description": "Scripts for automating upstream opencode merges into Kilo",