mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge branch 'main' into docs/custom-provider-options
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Hide reverted provider errors so Redo controls remain visible after rewinding a session.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep changed identifiers intact when highlighting edits within diff lines.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/sdk": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Limit completion sounds to parent agent sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Avoid failing Agent Manager startup when another extension already registered VS Code panel commands.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Prevent concurrent subagent updates from blanking the Agent Manager webview.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Speed up Agent Manager worktree hover cards so pull request details appear and dismiss more quickly.
|
||||
@@ -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.
|
||||
@@ -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'
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
+4
-4
@@ -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="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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": {}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.3.48",
|
||||
"version": "7.3.49",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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<Query, "url" | "dir">
|
||||
|
||||
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<KiloEmbeddingMo
|
||||
return demand("Kilo embedding models", await client(input).indexing.models())
|
||||
}
|
||||
|
||||
export async function loadKiloProfile(input: ProjectQuery): Promise<KiloProfileData> {
|
||||
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<ProviderAuthAuthorization> {
|
||||
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<ProjectItem[]> {
|
||||
const sdk = client(input)
|
||||
const dir = value(input.dir)
|
||||
|
||||
@@ -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()}
|
||||
</Route>
|
||||
<Route path="/profile" component={ProfileRoute} />
|
||||
<Route path="/kilo/login" component={LoginRoute} />
|
||||
<Route path="/settings" component={ConfigLayout}>
|
||||
{routes()}
|
||||
</Route>
|
||||
|
||||
@@ -265,7 +265,10 @@ export function AgentBuilderRoute() {
|
||||
onInput={(event) => state.setDesc(event.currentTarget.value)}
|
||||
/>
|
||||
</FieldCard>
|
||||
<FieldCard label="Mode">
|
||||
<FieldCard
|
||||
label="Mode"
|
||||
description="Primary agents can run sessions directly; subagents are delegated to by other agents."
|
||||
>
|
||||
<CustomSelect
|
||||
label="Agent mode"
|
||||
value={state.mode()}
|
||||
@@ -323,6 +326,7 @@ export function AgentBuilderRoute() {
|
||||
<div class="ui-form agent-builder-form">
|
||||
<FieldCard
|
||||
label="Model"
|
||||
description="Leave unset to inherit the default model, or choose one to pin this agent to a specific model."
|
||||
actions={
|
||||
<>
|
||||
<Show when={!state.locked() && state.model()}>
|
||||
|
||||
@@ -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() {
|
||||
<Stat label="Input" value={money(item.model.cost.input)} sub="/ 1M tok" mono />
|
||||
<Stat label="Output" value={money(item.model.cost.output)} sub="/ 1M tok" mono />
|
||||
<Stat label="Reasoning" value={item.model.capabilities.reasoning ? "Yes" : "No"} />
|
||||
<Stat label="Temperature" value={item.model.capabilities.temperature ? "Yes" : "No"} />
|
||||
</div>
|
||||
<Show when={chips(item.model).length}>
|
||||
<div class="tags model-capabilities">
|
||||
|
||||
@@ -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<State>({ status: "idle" })
|
||||
const [left, setLeft] = createSignal(900)
|
||||
const [attempt, setAttempt] = createSignal(0)
|
||||
const [active, setActive] = createSignal<AbortController>()
|
||||
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 (
|
||||
<section class="route-empty">
|
||||
<div class="profile-login-page">
|
||||
<header class="profile-login-header">
|
||||
<p class="eyebrow">Kilo Login</p>
|
||||
<h1>Login to Kilo</h1>
|
||||
<p>Authorize this Kilo Console through the same device auth flow used by the editor clients.</p>
|
||||
</header>
|
||||
|
||||
<Show when={!server.query() && server.discoverable()}>
|
||||
<LoadingScreen variant="fullscreen" />
|
||||
</Show>
|
||||
|
||||
<Show when={!server.query() && !server.discoverable()}>
|
||||
<Card class="profile-login-card" variant="warning">
|
||||
<strong>Kilo server not found</strong>
|
||||
<p>Start a local Kilo server or pass a server URL with ?server=.</p>
|
||||
<a class="profile-link-button" href={profile()}>
|
||||
Back to Profile
|
||||
</a>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
<Show when={server.query()}>
|
||||
<Card class="profile-login-card">
|
||||
<Switch>
|
||||
<Match when={auth().status === "idle" || auth().status === "initiating"}>
|
||||
<div class="profile-login-state">
|
||||
<span class="profile-spinner" aria-hidden="true" />
|
||||
<strong>Starting login...</strong>
|
||||
<p>Preparing a secure browser authorization request.</p>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={auth().status === "pending"}>
|
||||
<div class="profile-login-flow">
|
||||
<div class="profile-login-step">
|
||||
<span>Step 1</span>
|
||||
<strong>Open this URL</strong>
|
||||
<div class="profile-login-url">
|
||||
<span>{auth().url}</span>
|
||||
<Button variant="secondary" type="button" onClick={() => copy("url", auth().url)}>
|
||||
{copied() === "url" ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="primary" type="button" onClick={open}>
|
||||
Open Browser
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={auth().code}>
|
||||
{(code) => (
|
||||
<div class="profile-login-step">
|
||||
<span>Step 2</span>
|
||||
<strong>Enter this code</strong>
|
||||
<button type="button" class="profile-login-code" onClick={() => copy("code", code())}>
|
||||
{code()}
|
||||
<small>{copied() === "code" ? "Copied" : "Click to copy"}</small>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div class="profile-login-waiting">
|
||||
<span class="profile-spinner" aria-hidden="true" />
|
||||
<span>Waiting for authorization ({time(left())})</span>
|
||||
</div>
|
||||
|
||||
<Button variant="ghost" type="button" onClick={cancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={auth().status === "success"}>
|
||||
<div class="profile-login-state success">
|
||||
<strong>Login successful</strong>
|
||||
<p>Redirecting back to your profile.</p>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={auth().status === "error"}>
|
||||
<div class="profile-login-state error">
|
||||
<strong>Login failed</strong>
|
||||
<p>{auth().error}</p>
|
||||
<div class="profile-login-actions">
|
||||
<Button variant="primary" type="button" onClick={() => start()}>
|
||||
Try Again
|
||||
</Button>
|
||||
<a class="profile-link-button" href={profile()}>
|
||||
Back to Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={auth().status === "cancelled"}>
|
||||
<div class="profile-login-state">
|
||||
<strong>Login cancelled</strong>
|
||||
<p>No credentials were saved.</p>
|
||||
<Button variant="primary" type="button" onClick={() => start()}>
|
||||
Start Again
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Card>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -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<KiloProfileData["profile"]["organizations"]>[number]
|
||||
type State = { kind: "connected"; data: KiloProfileData } | { kind: "disconnected" }
|
||||
|
||||
async function load(input: ProjectQuery): Promise<State> {
|
||||
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<string>()
|
||||
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 (
|
||||
<section class="route-empty">
|
||||
<p class="eyebrow">Profile</p>
|
||||
<h1>Profile Settings</h1>
|
||||
<p>Profile identity, account preferences, and workspace defaults will live here.</p>
|
||||
<section class="config-shell profile-shell" classList={{ disconnected: disconnected() }}>
|
||||
<Show when={!disconnected()}>
|
||||
<aside class="config-sidebar" aria-label="Profile sections">
|
||||
<div class="config-sidebar-title">
|
||||
<span>Profile</span>
|
||||
<span class="config-sidebar-scope">
|
||||
<span>{scope()}</span>
|
||||
</span>
|
||||
</div>
|
||||
<nav class="config-options">
|
||||
<A class="config-top-option active" href={overview()} aria-current="page">
|
||||
<span>Overview</span>
|
||||
</A>
|
||||
<a class="config-top-option" href={usageUrl()} target="_blank" rel="noreferrer">
|
||||
<span>Usage</span>
|
||||
</a>
|
||||
<a class="config-top-option" href={creditsUrl()} target="_blank" rel="noreferrer">
|
||||
<span>Buy Credits</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
</Show>
|
||||
<section class="content">
|
||||
<div class="profile-page">
|
||||
<Show when={!disconnected()}>
|
||||
<header class="profile-header">
|
||||
<div>
|
||||
<p class="eyebrow">Kilo Account</p>
|
||||
<h1>Your Profile</h1>
|
||||
<p>Manage your Kilo identity, account context, credits, and billing shortcuts.</p>
|
||||
</div>
|
||||
<div class="profile-actions">
|
||||
<Button variant="secondary" type="button" onClick={refresh} disabled={data.loading || !server.query()}>
|
||||
Refresh
|
||||
</Button>
|
||||
<a
|
||||
class="profile-primary-link"
|
||||
data-component="button"
|
||||
data-size="default"
|
||||
data-variant="primary"
|
||||
href={cloud()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Open Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
</Show>
|
||||
|
||||
<Show when={!server.query() && server.discoverable()}>
|
||||
<LoadingScreen variant="fullscreen" />
|
||||
</Show>
|
||||
|
||||
<Show when={data.loading && !data()}>
|
||||
<LoadingScreen variant="fullscreen" />
|
||||
</Show>
|
||||
|
||||
<Show when={disconnected()}>
|
||||
<Card class="profile-connect-card">
|
||||
<span class="profile-connect-mark" aria-hidden="true">
|
||||
KG
|
||||
</span>
|
||||
<div class="profile-connect-copy">
|
||||
<p class="eyebrow">Kilo Account</p>
|
||||
<h1>Connect your Kilo account</h1>
|
||||
<p>Sign in to view your credits, organizations, and account details in Kilo Console.</p>
|
||||
</div>
|
||||
<A
|
||||
class="profile-primary-link"
|
||||
data-component="button"
|
||||
data-size="default"
|
||||
data-variant="primary"
|
||||
href={login()}
|
||||
>
|
||||
Connect
|
||||
</A>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
<Show when={!server.query() && !server.discoverable()}>
|
||||
<Card class="profile-banner" variant="warning">
|
||||
<strong>Kilo server not found</strong>
|
||||
<span>Start Kilo Console from a running Kilo server or pass a server URL with ?server=.</span>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
<Show when={data.error && !authError(data.error)}>
|
||||
<Card class="profile-banner" variant="error">
|
||||
<strong>Profile request failed</strong>
|
||||
<span>{errMsg(data.error)}</span>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
<Show when={error()}>
|
||||
<Card class="profile-banner" variant="error">
|
||||
<strong>Account update failed</strong>
|
||||
<span>{error()}</span>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
<Show when={profile()}>
|
||||
{(info) => (
|
||||
<div class="profile-grid">
|
||||
<Card class="profile-card profile-account-card">
|
||||
<div class="profile-account-head">
|
||||
<span class="profile-avatar" aria-hidden="true">
|
||||
{initials(info().profile.name, info().profile.email)}
|
||||
</span>
|
||||
<div class="profile-account-text">
|
||||
<strong>{info().profile.name || info().profile.email}</strong>
|
||||
<span>{info().profile.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-meta-row">
|
||||
<span>Active account</span>
|
||||
<strong>{account(info())}</strong>
|
||||
</div>
|
||||
<div class="profile-card-actions">
|
||||
<Button variant="ghost" type="button" onClick={logout} disabled={Boolean(saving())}>
|
||||
Log Out
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="profile-card profile-balance-card">
|
||||
<span class="profile-card-label">Remaining Credits</span>
|
||||
<strong>{money(info().balance?.balance)}</strong>
|
||||
<p>
|
||||
Credits shown for {account(info())}. Switch accounts below to see organization balances when
|
||||
available.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card class="profile-card profile-wide-card">
|
||||
<header class="profile-card-head">
|
||||
<div>
|
||||
<span class="profile-card-label">Organizations</span>
|
||||
<h2>Your Accounts</h2>
|
||||
</div>
|
||||
<span class="profile-pill">{orgs().length === 1 ? "1 org" : `${orgs().length} orgs`}</span>
|
||||
</header>
|
||||
<div class="profile-org-list">
|
||||
<button
|
||||
type="button"
|
||||
class="profile-org-row"
|
||||
classList={{ active: !info().currentOrgId }}
|
||||
onClick={() => choose(null)}
|
||||
disabled={Boolean(saving())}
|
||||
>
|
||||
<span class="profile-org-icon" aria-hidden="true">
|
||||
KG
|
||||
</span>
|
||||
<span class="profile-org-body">
|
||||
<strong>Personal Account</strong>
|
||||
<span>Your personal Kilo credits and settings</span>
|
||||
</span>
|
||||
<span class="profile-org-state">{!info().currentOrgId ? "Current" : "Use"}</span>
|
||||
</button>
|
||||
<For each={orgs()}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
class="profile-org-row"
|
||||
classList={{ active: active()?.id === item.id }}
|
||||
onClick={() => choose(item.id)}
|
||||
disabled={Boolean(saving())}
|
||||
>
|
||||
<span class="profile-org-icon" aria-hidden="true">
|
||||
{initials(item.name, item.name)}
|
||||
</span>
|
||||
<span class="profile-org-body">
|
||||
<strong>{item.name}</strong>
|
||||
<span>{role(item)}</span>
|
||||
</span>
|
||||
<span class="profile-org-state">{active()?.id === item.id ? "Current" : "Use"}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
})
|
||||
@@ -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<string, string | null | undefined>) {
|
||||
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)}`)
|
||||
}
|
||||
@@ -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<ProjectQuery | undefined>(() => {
|
||||
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 }
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e6ecf78a0bcfaebdb945c3caacb8e32155fe2f6e4017f766e129dd0d17ad8fab
|
||||
size 51349
|
||||
oid sha256:270d3ecab98c4462a0e6af3e8fe43b3313da03759c507914d0b1048045474f1e
|
||||
size 50886
|
||||
|
||||
@@ -81,8 +81,6 @@
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceContribute.tsx -->
|
||||
- <https://github.com/Kilo-Org/kilocode>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx -->
|
||||
- <https://github.com/Kilo-Org/kilocode/>
|
||||
<!-- packages/kilo-vscode/webview-ui/src/context/notifications.tsx -->
|
||||
- <https://github.com/Kilo-Org/kilocode/issues/6986>
|
||||
<!-- packages/kilo-vscode/src/agent-manager/constants.ts -->
|
||||
- <https://github.com/Kilo-Org/kilocode/issues/9618>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 && {
|
||||
|
||||
@@ -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<string, string>, 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) {
|
||||
|
||||
@@ -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 } } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ export interface KiloProviderOptions {
|
||||
*/
|
||||
name?: string
|
||||
|
||||
/**
|
||||
* Data collection preference for upstream provider routing
|
||||
*/
|
||||
dataCollection?: "allow" | "deny"
|
||||
|
||||
/**
|
||||
* Custom fetch function
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
@@ -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),
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -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<String>,
|
||||
|
||||
+3
-1
@@ -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<ProviderList200Response>(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,
|
||||
|
||||
+4
@@ -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)
|
||||
|
||||
+3
-1
@@ -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<ProviderList200Response>(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,
|
||||
|
||||
+1
@@ -404,6 +404,7 @@ class SessionUi(
|
||||
it.providerName,
|
||||
it.recommendedIndex,
|
||||
it.free,
|
||||
it.byok,
|
||||
it.variants,
|
||||
it.attachment,
|
||||
)
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
@@ -583,6 +583,7 @@ data class ModelItem(
|
||||
val providerName: String,
|
||||
val recommendedIndex: Double?,
|
||||
val free: Boolean,
|
||||
val byok: Boolean = false,
|
||||
val variants: List<String>,
|
||||
val limit: ModelLimitItem?,
|
||||
val attachment: Boolean = false,
|
||||
|
||||
+1
@@ -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<String> = emptyList(),
|
||||
val attachment: Boolean = false,
|
||||
) {
|
||||
|
||||
+14
-1
@@ -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
|
||||
|
||||
+10
-1
@@ -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
|
||||
}
|
||||
|
||||
+17
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {}
|
||||
|
||||
@@ -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<String> = emptyList(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.48",
|
||||
"version": "7.3.49",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<unknown> {
|
||||
@@ -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<unknown>): 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ export class AttentionService implements vscode.Disposable {
|
||||
private readonly errored = new Set<string>()
|
||||
private readonly questions = new Set<string>()
|
||||
private readonly permissions = new Set<string>()
|
||||
private readonly parents = new Map<string, string | undefined>()
|
||||
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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "7.3.48",
|
||||
"version": "7.3.49",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
/<Show when=\{isFree\(model\)\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\)\}>/,
|
||||
)
|
||||
expect(preview).toMatch(
|
||||
/<Show when=\{model\(\)\.isFree\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\(\)\)\}>/,
|
||||
)
|
||||
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</Tag>")
|
||||
expect(preview).toContain(">BYOK</span>")
|
||||
expect(agent).toContain(">BYOK</span>")
|
||||
})
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<Message> = {}): 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", () => {
|
||||
|
||||
@@ -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<unknown>
|
||||
|
||||
function runtime(run: () => Promise<unknown>) {
|
||||
let blocked = false
|
||||
const handlers = new Map<string, Handler>()
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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"])
|
||||
|
||||
@@ -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<{
|
||||
}
|
||||
/>
|
||||
<span class="am-mm-item-name">{model.name}</span>
|
||||
<Show when={model.isFree}>
|
||||
<Show when={model.isFree || hasByok(model) || isDataCollectedModel(model)}>
|
||||
<span class="am-mm-free-data">
|
||||
<span class="am-mm-free-badge">{freeLabel()}</span>
|
||||
<Show when={model.isFree && !hasByok(model)}>
|
||||
<span class="am-mm-free-badge">{freeLabel()}</span>
|
||||
</Show>
|
||||
<Show when={hasByok(model)}>
|
||||
<span class="am-mm-byok-badge">BYOK</span>
|
||||
</Show>
|
||||
<Show when={isDataCollectedModel(model)}>
|
||||
<Tooltip value={dataLabel()} placement="top">
|
||||
<span class="am-mm-free-data-icon" aria-label={dataLabel()}>
|
||||
|
||||
@@ -151,8 +151,9 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
|
||||
</Show>
|
||||
<ContextMenu>
|
||||
<HoverCard
|
||||
openDelay={100}
|
||||
closeDelay={100}
|
||||
class="am-worktree-hover-card"
|
||||
openDelay={50}
|
||||
closeDelay={50}
|
||||
placement="right-start"
|
||||
gutter={8}
|
||||
open={hovered() && !overClose() && !props.pendingDelete}
|
||||
|
||||
@@ -7,6 +7,47 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Virtua already removes off-screen transcript rows. Avoid a second layer of
|
||||
layout skipping, which can leave Chromium accessibility traversing fragments
|
||||
whose layout items were discarded while Agent Manager scrolls. */
|
||||
.am-layout
|
||||
:is(
|
||||
[data-component="assistant-message"],
|
||||
[data-component="tool-trigger"],
|
||||
[data-component="file"],
|
||||
[data-component="code"],
|
||||
[data-component="diff"]
|
||||
) {
|
||||
content-visibility: visible;
|
||||
}
|
||||
|
||||
/* Generated separators become accessibility text nodes. Keep them out of
|
||||
concurrently updating Agent Manager tool rows while preserving sidebar UI. */
|
||||
html[data-theme="kilo-vscode"]
|
||||
.am-layout
|
||||
[data-component="tool-part-wrapper"][data-part-type="tool"]
|
||||
[data-component="collapsible"]
|
||||
[data-slot="basic-tool-tool-title"]
|
||||
+ :is(
|
||||
[data-slot="basic-tool-tool-subtitle"],
|
||||
[data-slot="basic-tool-tool-arg"],
|
||||
[data-slot="message-part-meta-line"],
|
||||
[data-slot="webfetch-meta"],
|
||||
[data-component="shell-submessage"]
|
||||
)::before,
|
||||
html[data-theme="kilo-vscode"]
|
||||
.am-layout
|
||||
[data-component="tool-part-wrapper"][data-part-type="tool"]
|
||||
[data-component="collapsible"]
|
||||
[data-slot="message-part-title-text"]
|
||||
+ :is([data-slot="message-part-meta-line"], [data-slot="basic-tool-tool-subtitle"])::before,
|
||||
html[data-theme="kilo-vscode"]
|
||||
.am-layout
|
||||
[data-component="tool-part-wrapper"][data-part-type="tool"]
|
||||
[data-slot="context-tool-group-summary"]::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.am-sidebar {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
@@ -2891,18 +2932,27 @@ body.am-wt-dragging-active * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.am-mm-free-badge {
|
||||
.am-mm-free-badge,
|
||||
.am-mm-byok-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #fff;
|
||||
background: var(--vscode-testing-iconPassed, #4caf50);
|
||||
font-size: var(--kilo-font-size-10);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.am-mm-free-badge {
|
||||
color: #fff;
|
||||
background: var(--vscode-testing-iconPassed, #4caf50);
|
||||
}
|
||||
|
||||
.am-mm-byok-badge {
|
||||
color: var(--vscode-badge-foreground);
|
||||
background: var(--vscode-badge-background);
|
||||
}
|
||||
|
||||
.am-mm-free-data-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -3076,6 +3126,14 @@ body.am-wt-dragging-active * {
|
||||
|
||||
/* HoverCard popover for worktree items */
|
||||
|
||||
.am-worktree-hover-card[data-expanded] {
|
||||
animation-duration: 80ms;
|
||||
}
|
||||
|
||||
.am-worktree-hover-card[data-closed] {
|
||||
animation-duration: 60ms;
|
||||
}
|
||||
|
||||
.am-hover-card {
|
||||
padding: 10px 12px;
|
||||
min-width: 160px;
|
||||
|
||||
@@ -332,6 +332,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
suggestions={standaloneSuggestions}
|
||||
readonly={props.readonly}
|
||||
emptyState={props.emptyState}
|
||||
announce={isSidebar()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<MessageListProps> = (props) => {
|
||||
@@ -73,6 +75,19 @@ export const MessageList: Component<MessageListProps> = (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<MessageListProps> = (props) => {
|
||||
const [virtualizer, setVirtualizer] = createSignal<VirtualizerHandle>()
|
||||
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<MessageListProps> = (props) => {
|
||||
queued: queuedIDs(),
|
||||
live: new Set(active ? [active] : []),
|
||||
hidden: session.isErrorHidden,
|
||||
revert: revert(),
|
||||
},
|
||||
prev,
|
||||
)
|
||||
@@ -260,13 +276,25 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div class="message-list-container">
|
||||
<Show when={props.announce === false}>
|
||||
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
||||
{announcement()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={isEmpty()}>
|
||||
<div class="welcome-header">
|
||||
<AccountSwitcher class="account-switcher-welcome" />
|
||||
<KiloNotifications />
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={setScrollRef} onScroll={handleScroll} class="message-list" role="log" aria-live="polite">
|
||||
<div
|
||||
ref={setScrollRef}
|
||||
onScroll={handleScroll}
|
||||
class="message-list"
|
||||
role={props.announce === false ? undefined : "log"}
|
||||
aria-live={props.announce === false ? undefined : "polite"}
|
||||
aria-busy={props.announce === false && session.status() !== "idle" ? "true" : undefined}
|
||||
>
|
||||
<div ref={autoScroll.contentRef} class={isEmpty() ? "message-list-content-empty" : "message-list-content"}>
|
||||
<Show when={session.loading()}>
|
||||
<div class="message-list-loading" role="status">
|
||||
@@ -326,7 +354,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={boundary()}>
|
||||
<Show when={revert()}>
|
||||
<RevertBanner />
|
||||
</Show>
|
||||
<For each={partition().queued}>{(row) => <TranscriptRowView row={row} />}</For>
|
||||
|
||||
@@ -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<ToolProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={result()}>{(text) => <Markdown text={text()} />}</Show>
|
||||
<For each={childToolParts()}>
|
||||
<Index each={childToolParts()}>
|
||||
{(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<ToolProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Index>
|
||||
</div>
|
||||
</div>
|
||||
</BasicTool>
|
||||
|
||||
@@ -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> = (props) => {
|
||||
})()}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={model().isFree || isDataCollectedModel(model())}>
|
||||
<Show when={model().isFree || hasByok(model()) || isDataCollectedModel(model())}>
|
||||
<span class="model-preview-free-data">
|
||||
<Show when={model().isFree}>
|
||||
<Show when={model().isFree && !hasByok(model())}>
|
||||
<span class="model-preview-badge model-preview-badge--free">{freeLabel()}</span>
|
||||
</Show>
|
||||
<Show when={hasByok(model())}>
|
||||
<span class="model-preview-badge">BYOK</span>
|
||||
</Show>
|
||||
<Show when={isDataCollectedModel(model())}>
|
||||
<Tooltip value={dataLabel()} placement="top">
|
||||
<span class="model-preview-free-data-icon" aria-label={dataLabel()}>
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
providerSortKey,
|
||||
isFree,
|
||||
isDataCollectedModel,
|
||||
hasByok,
|
||||
freeDataLabel,
|
||||
buildTriggerLabel,
|
||||
sanitizeName,
|
||||
@@ -936,13 +937,18 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
<Show when={isFree(model) || isDataCollectedModel(model)}>
|
||||
<Show when={isFree(model) || hasByok(model) || isDataCollectedModel(model)}>
|
||||
<span class="model-selector-free-data">
|
||||
<Show when={isFree(model)}>
|
||||
<Show when={isFree(model) && !hasByok(model)}>
|
||||
<span class="model-selector-data-badge">
|
||||
<Tag data-variant="member">{freeLabel()}</Tag>
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={hasByok(model)}>
|
||||
<span class="model-selector-data-badge model-selector-data-badge--byok">
|
||||
<Tag data-variant="member">BYOK</Tag>
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={isDataCollectedModel(model)}>
|
||||
<Tooltip value={dataLabel()} placement="top">
|
||||
<span class="model-selector-free-data-icon" aria-label={dataLabel()}>
|
||||
|
||||
@@ -26,6 +26,10 @@ export function isDataCollectedModel(model: Pick<EnrichedModel, "mayTrainOnYourP
|
||||
return model.mayTrainOnYourPrompts === true
|
||||
}
|
||||
|
||||
export function hasByok(model: Pick<EnrichedModel, "hasUserByokAvailable">): boolean {
|
||||
return model.hasUserByokAvailable === true
|
||||
}
|
||||
|
||||
export function freeDataLabel(_free: string, data: string): string {
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -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<KilocodeNotification[]>
|
||||
filteredNotifications: Accessor<KilocodeNotification[]>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Message, SessionStatusInfo } from "../types/messages"
|
||||
import type { Message, Part, SessionInfo, SessionStatusInfo } from "../types/messages"
|
||||
|
||||
export type RevertBoundary = Pick<NonNullable<SessionInfo["revert"]>, "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<string, { turn: MessageTurn; index: number }>()
|
||||
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],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, Part[]>) {
|
||||
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<string, Part[]> {
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string>
|
||||
live?: ReadonlySet<string>
|
||||
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 })
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ProviderModel {
|
||||
recommendedIndex?: number
|
||||
isFree?: boolean
|
||||
mayTrainOnYourPrompts?: boolean
|
||||
hasUserByokAvailable?: boolean
|
||||
terminalBench?: {
|
||||
overallScore: number
|
||||
avgAttemptCostUsd: number
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user