From abcc26f6dc968cde23464690b8768d8fed36922a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Mon, 9 Mar 2026 14:55:40 +0100 Subject: [PATCH 01/17] fix: improve auto-scroll reliability during streaming - Remove onClick handleInteraction from MessageList to stop clicks breaking auto-follow - Always resume auto-scroll when working becomes true (re-enables on follow-up messages) - Simplify isAuto to pure time-based gating (removes fragile position-comparison race) - Debounce stop() in handleScroll to ignore layout-induced scroll shifts --- .../src/components/chat/MessageList.tsx | 1 - packages/ui/src/hooks/create-auto-scroll.tsx | 31 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 6764da91916..f44b74f7236 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -77,7 +77,6 @@ export const MessageList: Component = (props) => {
boolean onUserInteracted?: () => void @@ -14,8 +16,9 @@ export function createAutoScroll(options: AutoScrollOptions) { let settling = false let settleTimer: ReturnType | undefined let autoTimer: ReturnType | undefined + let stopTimer: ReturnType | undefined let cleanup: (() => void) | undefined - let auto: { top: number; time: number } | undefined + let auto: { time: number } | undefined const threshold = () => options.bottomThreshold ?? 10 @@ -38,11 +41,8 @@ export function createAutoScroll(options: AutoScrollOptions) { // between us calling `scrollTo()` and the subsequent `scroll` event firing, // the handler can see a non-zero `distanceFromBottom` and incorrectly assume // the user scrolled. - const markAuto = (el: HTMLElement) => { - auto = { - top: Math.max(0, el.scrollHeight - el.clientHeight), - time: Date.now(), - } + const markAuto = (_el: HTMLElement) => { + auto = { time: Date.now() } if (autoTimer) clearTimeout(autoTimer) autoTimer = setTimeout(() => { @@ -51,7 +51,7 @@ export function createAutoScroll(options: AutoScrollOptions) { }, 250) } - const isAuto = (el: HTMLElement) => { + const isAuto = (_el: HTMLElement) => { const a = auto if (!a) return false @@ -60,7 +60,7 @@ export function createAutoScroll(options: AutoScrollOptions) { return false } - return Math.abs(el.scrollTop - a.top) < 2 + return true } const scrollToBottomNow = (behavior: ScrollBehavior) => { @@ -137,7 +137,17 @@ export function createAutoScroll(options: AutoScrollOptions) { return } - stop() + // Debounce to avoid layout-induced scroll shifts (e.g. images loading, + // virtual-list reflows) from incorrectly breaking auto-follow. + if (stopTimer) clearTimeout(stopTimer) + stopTimer = setTimeout(() => { + stopTimer = undefined + const cur = scroll + if (!cur) return + if (distanceFromBottom(cur) < threshold()) return + if (!store.userScrolled && isAuto(cur)) return + stop() + }, DEBOUNCE_MS) } const handleInteraction = () => { @@ -185,7 +195,7 @@ export function createAutoScroll(options: AutoScrollOptions) { settleTimer = undefined if (working) { - if (!store.userScrolled) scrollToBottom(true) + scrollToBottom(true) return } @@ -208,6 +218,7 @@ export function createAutoScroll(options: AutoScrollOptions) { onCleanup(() => { if (settleTimer) clearTimeout(settleTimer) if (autoTimer) clearTimeout(autoTimer) + if (stopTimer) clearTimeout(stopTimer) if (cleanup) cleanup() }) From 6cb678a1bbe5bc188041e38f8c3a36a09b44bffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Mon, 9 Mar 2026 15:55:30 +0100 Subject: [PATCH 02/17] fix: move auto-scroll improvements to kilo-ui to prevent upstream merge conflicts --- bun.lock | 67 ++++- packages/kilo-ui/package.json | 1 + .../kilo-ui/src/hooks/create-auto-scroll.tsx | 256 ++++++++++++++++++ packages/kilo-ui/src/hooks/index.ts | 3 +- packages/ui/src/hooks/create-auto-scroll.tsx | 31 +-- 5 files changed, 332 insertions(+), 26 deletions(-) create mode 100644 packages/kilo-ui/src/hooks/create-auto-scroll.tsx diff --git a/bun.lock b/bun.lock index f1f2541661a..c190b8cdfde 100644 --- a/bun.lock +++ b/bun.lock @@ -204,6 +204,7 @@ "version": "7.0.40", "dependencies": { "@kobalte/core": "0.13.11", + "@solid-primitives/resize-observer": "2.1.5", "lucide-solid": "0.576.0", }, "devDependencies": { @@ -1586,7 +1587,7 @@ "@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="], - "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="], "@solid-primitives/i18n": ["@solid-primitives/i18n@2.2.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TnTnE2Ku11MGYZ1JzhJ8pYscwg1fr9MteoYxPwsfxWfh9Jp5K7RRJncJn9BhOHvNLwROjqOHZ46PT7sPHqbcXw=="], @@ -1600,15 +1601,15 @@ "@solid-primitives/refs": ["@solid-primitives/refs@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg=="], - "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="], - "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], "@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="], "@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="], - "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ=="], "@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="], @@ -4206,6 +4207,10 @@ "@kilocode/kilo-gateway/@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="], + "@kobalte/core/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + + "@kobalte/utils/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], @@ -4250,6 +4255,8 @@ "@opencode-ai/app/@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], + "@opencode-ai/app/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@opencode-ai/app/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], "@opencode-ai/app/vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="], @@ -4260,6 +4267,8 @@ "@opencode-ai/desktop/vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="], + "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@opencode-ai/ui/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], "@opencode-ai/ui/vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="], @@ -4302,6 +4311,36 @@ "@shikijs/themes/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@solid-primitives/active-element/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + + "@solid-primitives/audio/@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + + "@solid-primitives/bounds/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + + "@solid-primitives/bounds/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + + "@solid-primitives/bounds/@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + + "@solid-primitives/event-listener/@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], + + "@solid-primitives/media/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + + "@solid-primitives/media/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + + "@solid-primitives/media/@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + + "@solid-primitives/resize-observer/@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], + + "@solid-primitives/rootless/@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], + + "@solid-primitives/scroll/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + + "@solid-primitives/scroll/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + + "@solid-primitives/scroll/@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + + "@solid-primitives/static-store/@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], + "@storybook/addon-docs/react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], "@storybook/addon-docs/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], @@ -5244,6 +5283,12 @@ "@kilocode/kilo-gateway/@opentui/solid/solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], + "@kobalte/core/@solid-primitives/resize-observer/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + + "@kobalte/core/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + + "@kobalte/core/@solid-primitives/resize-observer/@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -5278,12 +5323,24 @@ "@opencode-ai/app/@playwright/test/playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], + "@opencode-ai/app/@solid-primitives/resize-observer/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + + "@opencode-ai/app/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + + "@opencode-ai/app/@solid-primitives/resize-observer/@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + "@opencode-ai/app/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], "@opencode-ai/desktop/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "@opencode-ai/ui/@solid-primitives/resize-observer/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + + "@opencode-ai/ui/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + + "@opencode-ai/ui/@solid-primitives/resize-observer/@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + "@opencode-ai/ui/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], @@ -5314,6 +5371,8 @@ "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@solid-primitives/bounds/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + "@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "@tailwindcss/node/lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index c35311b8452..cade0fad54f 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -106,6 +106,7 @@ }, "dependencies": { "@kobalte/core": "0.13.11", + "@solid-primitives/resize-observer": "2.1.5", "lucide-solid": "0.576.0" } } diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx new file mode 100644 index 00000000000..3762d8fcfa3 --- /dev/null +++ b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx @@ -0,0 +1,256 @@ +// kilocode_change - new file +import { createEffect, on, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" +import { createResizeObserver } from "@solid-primitives/resize-observer" + +const DEBOUNCE_MS = 100 + +export interface AutoScrollOptions { + working: () => boolean + onUserInteracted?: () => void + overflowAnchor?: "none" | "auto" | "dynamic" + bottomThreshold?: number +} + +export function createAutoScroll(options: AutoScrollOptions) { + let scroll: HTMLElement | undefined + let settling = false + let settleTimer: ReturnType | undefined + let autoTimer: ReturnType | undefined + let stopTimer: ReturnType | undefined + let cleanup: (() => void) | undefined + let auto: { time: number } | undefined + + const threshold = () => options.bottomThreshold ?? 10 + + const [store, setStore] = createStore({ + contentRef: undefined as HTMLElement | undefined, + userScrolled: false, + }) + + const active = () => options.working() || settling + + const distanceFromBottom = (el: HTMLElement) => { + return el.scrollHeight - el.clientHeight - el.scrollTop + } + + const canScroll = (el: HTMLElement) => { + return el.scrollHeight - el.clientHeight > 1 + } + + // Browsers can dispatch scroll events asynchronously. If new content arrives + // between us calling `scrollTo()` and the subsequent `scroll` event firing, + // the handler can see a non-zero `distanceFromBottom` and incorrectly assume + // the user scrolled. + const markAuto = (_el: HTMLElement) => { + auto = { time: Date.now() } + + if (autoTimer) clearTimeout(autoTimer) + autoTimer = setTimeout(() => { + auto = undefined + autoTimer = undefined + }, 250) + } + + const isAuto = (_el: HTMLElement) => { + const a = auto + if (!a) return false + + if (Date.now() - a.time > 250) { + auto = undefined + return false + } + + return true + } + + const scrollToBottomNow = (behavior: ScrollBehavior) => { + const el = scroll + if (!el) return + markAuto(el) + if (behavior === "smooth") { + el.scrollTo({ top: el.scrollHeight, behavior }) + return + } + + // `scrollTop` assignment bypasses any CSS `scroll-behavior: smooth`. + el.scrollTop = el.scrollHeight + } + + const scrollToBottom = (force: boolean) => { + if (!force && !active()) return + const el = scroll + if (!el) return + + if (!force && store.userScrolled) return + if (force && store.userScrolled) setStore("userScrolled", false) + + const distance = distanceFromBottom(el) + if (distance < 2) return + + // For auto-following content we prefer immediate updates to avoid + // visible "catch up" animations while content is still settling. + scrollToBottomNow("auto") + } + + const stop = () => { + const el = scroll + if (!el) return + if (!canScroll(el)) { + if (store.userScrolled) setStore("userScrolled", false) + return + } + if (store.userScrolled) return + + setStore("userScrolled", true) + options.onUserInteracted?.() + } + + const handleWheel = (e: WheelEvent) => { + if (e.deltaY >= 0) return + // If the user is scrolling within a nested scrollable region (tool output, + // code block, etc), don't treat it as leaving the "follow bottom" mode. + // Those regions opt in via `data-scrollable`. + const el = scroll + const target = e.target instanceof Element ? e.target : undefined + const nested = target?.closest("[data-scrollable]") + if (el && nested && nested !== el) return + stop() + } + + const handleScroll = () => { + const el = scroll + if (!el) return + + if (!canScroll(el)) { + if (store.userScrolled) setStore("userScrolled", false) + return + } + + if (distanceFromBottom(el) < threshold()) { + if (store.userScrolled) setStore("userScrolled", false) + return + } + + // Ignore scroll events triggered by our own scrollToBottom calls. + if (!store.userScrolled && isAuto(el)) { + scrollToBottom(false) + return + } + + // Debounce to avoid layout-induced scroll shifts (e.g. images loading, + // virtual-list reflows) from incorrectly breaking auto-follow. + if (stopTimer) clearTimeout(stopTimer) + stopTimer = setTimeout(() => { + stopTimer = undefined + const cur = scroll + if (!cur) return + if (distanceFromBottom(cur) < threshold()) return + if (!store.userScrolled && isAuto(cur)) return + stop() + }, DEBOUNCE_MS) + } + + const handleInteraction = () => { + if (!active()) return + stop() + } + + const updateOverflowAnchor = (el: HTMLElement) => { + const mode = options.overflowAnchor ?? "dynamic" + + if (mode === "none") { + el.style.overflowAnchor = "none" + return + } + + if (mode === "auto") { + el.style.overflowAnchor = "auto" + return + } + + el.style.overflowAnchor = store.userScrolled ? "auto" : "none" + } + + createResizeObserver( + () => store.contentRef, + () => { + const el = scroll + if (el && !canScroll(el)) { + if (store.userScrolled) setStore("userScrolled", false) + return + } + if (!active()) return + if (store.userScrolled) return + // ResizeObserver fires after layout, before paint. + // Keep the bottom locked in the same frame to avoid visible + // "jump up then catch up" artifacts while streaming content. + scrollToBottom(false) + }, + ) + + createEffect( + on(options.working, (working: boolean) => { + settling = false + if (settleTimer) clearTimeout(settleTimer) + settleTimer = undefined + + if (working) { + scrollToBottom(true) + return + } + + settling = true + settleTimer = setTimeout(() => { + settling = false + }, 300) + }), + ) + + createEffect(() => { + // Track `userScrolled` even before `scrollRef` is attached, so we can + // update overflow anchoring once the element exists. + store.userScrolled + const el = scroll + if (!el) return + updateOverflowAnchor(el) + }) + + onCleanup(() => { + if (settleTimer) clearTimeout(settleTimer) + if (autoTimer) clearTimeout(autoTimer) + if (stopTimer) clearTimeout(stopTimer) + if (cleanup) cleanup() + }) + + return { + scrollRef: (el: HTMLElement | undefined) => { + if (cleanup) { + cleanup() + cleanup = undefined + } + + scroll = el + + if (!el) return + + updateOverflowAnchor(el) + el.addEventListener("wheel", handleWheel, { passive: true }) + + cleanup = () => { + el.removeEventListener("wheel", handleWheel) + } + }, + contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el), + handleScroll, + handleInteraction, + pause: stop, + resume: () => { + if (store.userScrolled) setStore("userScrolled", false) + scrollToBottom(true) + }, + scrollToBottom: () => scrollToBottom(false), + forceScrollToBottom: () => scrollToBottom(true), + userScrolled: () => store.userScrolled, + } +} diff --git a/packages/kilo-ui/src/hooks/index.ts b/packages/kilo-ui/src/hooks/index.ts index c3b66ced39d..5e8d24ba0f8 100644 --- a/packages/kilo-ui/src/hooks/index.ts +++ b/packages/kilo-ui/src/hooks/index.ts @@ -1 +1,2 @@ -export * from "@opencode-ai/ui/hooks" +export { useFilteredList } from "@opencode-ai/ui/hooks" +export * from "./create-auto-scroll" diff --git a/packages/ui/src/hooks/create-auto-scroll.tsx b/packages/ui/src/hooks/create-auto-scroll.tsx index 99862e4ae73..c32017739cd 100644 --- a/packages/ui/src/hooks/create-auto-scroll.tsx +++ b/packages/ui/src/hooks/create-auto-scroll.tsx @@ -2,8 +2,6 @@ import { createEffect, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { createResizeObserver } from "@solid-primitives/resize-observer" -const DEBOUNCE_MS = 100 - export interface AutoScrollOptions { working: () => boolean onUserInteracted?: () => void @@ -16,9 +14,8 @@ export function createAutoScroll(options: AutoScrollOptions) { let settling = false let settleTimer: ReturnType | undefined let autoTimer: ReturnType | undefined - let stopTimer: ReturnType | undefined let cleanup: (() => void) | undefined - let auto: { time: number } | undefined + let auto: { top: number; time: number } | undefined const threshold = () => options.bottomThreshold ?? 10 @@ -41,8 +38,11 @@ export function createAutoScroll(options: AutoScrollOptions) { // between us calling `scrollTo()` and the subsequent `scroll` event firing, // the handler can see a non-zero `distanceFromBottom` and incorrectly assume // the user scrolled. - const markAuto = (_el: HTMLElement) => { - auto = { time: Date.now() } + const markAuto = (el: HTMLElement) => { + auto = { + top: Math.max(0, el.scrollHeight - el.clientHeight), + time: Date.now(), + } if (autoTimer) clearTimeout(autoTimer) autoTimer = setTimeout(() => { @@ -51,7 +51,7 @@ export function createAutoScroll(options: AutoScrollOptions) { }, 250) } - const isAuto = (_el: HTMLElement) => { + const isAuto = (el: HTMLElement) => { const a = auto if (!a) return false @@ -60,7 +60,7 @@ export function createAutoScroll(options: AutoScrollOptions) { return false } - return true + return Math.abs(el.scrollTop - a.top) < 2 } const scrollToBottomNow = (behavior: ScrollBehavior) => { @@ -137,17 +137,7 @@ export function createAutoScroll(options: AutoScrollOptions) { return } - // Debounce to avoid layout-induced scroll shifts (e.g. images loading, - // virtual-list reflows) from incorrectly breaking auto-follow. - if (stopTimer) clearTimeout(stopTimer) - stopTimer = setTimeout(() => { - stopTimer = undefined - const cur = scroll - if (!cur) return - if (distanceFromBottom(cur) < threshold()) return - if (!store.userScrolled && isAuto(cur)) return - stop() - }, DEBOUNCE_MS) + stop() } const handleInteraction = () => { @@ -195,7 +185,7 @@ export function createAutoScroll(options: AutoScrollOptions) { settleTimer = undefined if (working) { - scrollToBottom(true) + if (!store.userScrolled) scrollToBottom(true) return } @@ -218,7 +208,6 @@ export function createAutoScroll(options: AutoScrollOptions) { onCleanup(() => { if (settleTimer) clearTimeout(settleTimer) if (autoTimer) clearTimeout(autoTimer) - if (stopTimer) clearTimeout(stopTimer) if (cleanup) cleanup() }) From c6950f8365e35cf12033cb725e1f03dc93045605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Mon, 9 Mar 2026 12:37:42 -0300 Subject: [PATCH 03/17] fix(vscode): resolve permission prompt hangs in VS Code extension - Handle permission.replied SSE events: map them to permissionResolved messages so cascaded and server-resolved permissions clear the webview prompt without user action - Remove optimistic permission removal: replace with a respondingPermissions Set that disables buttons while awaiting server confirmation, then the SSE-driven permissionResolved clears the prompt - Notify webview on HTTP failure: handlePermissionResponse now posts permissionError to re-enable buttons and show a toast instead of silently swallowing the error - Migrate from deprecated permission.respond to permission.reply endpoint, fixing a missing await bug in the legacy server handler - Recover missed permissions on reconnect and session load: new fetchAndSendPendingPermissions() calls GET /permission after SSE reconnects and after loadMessages, so events lost during reconnection or while the webview was not ready are replayed --- packages/kilo-vscode/src/KiloProvider.ts | 44 +++++++- .../kilo-vscode/src/kilo-provider-utils.ts | 7 ++ .../tests/unit/kilo-provider-utils.test.ts | 37 +++++++ .../src/components/chat/AssistantMessage.tsx | 100 +++++++++--------- .../src/components/chat/ChatView.tsx | 29 +++-- .../webview-ui/src/context/session.tsx | 45 +++++++- .../webview-ui/src/types/messages.ts | 12 +++ 7 files changed, 211 insertions(+), 63 deletions(-) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 16f867798fd..6f1e873287a 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -647,6 +647,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } await this.syncWebviewState("sse-connected") await this.flushPendingSessionRefresh("sse-connected") + await this.fetchAndSendPendingPermissions() } catch (error) { console.error("[Kilo New] KiloProvider: ❌ Failed during connected state handling:", error) this.postMessage({ @@ -825,6 +826,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper sessionID, messages, }) + + // Recover any permission.asked events that were missed while the webview + // was loading or during an SSE reconnection (fire-and-forget). + void this.fetchAndSendPendingPermissions() } catch (error) { // Silently ignore aborted requests — the user switched to a different session if (abort.signal.aborted) return @@ -1515,23 +1520,58 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper response: "once" | "always" | "reject", ): Promise { if (!this.client) { + this.postMessage({ type: "permissionError", permissionID: permissionId }) return } const targetSessionID = sessionID || this.currentSession?.id if (!targetSessionID) { console.error("[Kilo New] KiloProvider: No sessionID for permission response") + this.postMessage({ type: "permissionError", permissionID: permissionId }) return } try { const workspaceDir = this.getWorkspaceDirectory(targetSessionID) - await this.client.permission.respond( - { sessionID: targetSessionID, permissionID: permissionId, response, directory: workspaceDir }, + await this.client.permission.reply( + { requestID: permissionId, reply: response, directory: workspaceDir }, { throwOnError: true }, ) } catch (error) { console.error("[Kilo New] KiloProvider: Failed to respond to permission:", error) + this.postMessage({ type: "permissionError", permissionID: permissionId }) + } + } + + /** + * Fetch all pending permissions from the backend and forward any that belong + * to tracked sessions to the webview. Called after SSE reconnects and after + * loading messages for a session so that missed permission.asked events are + * recovered instead of leaving the server blocked indefinitely. + */ + private async fetchAndSendPendingPermissions(): Promise { + if (!this.client) return + try { + const workspaceDir = this.getWorkspaceDirectory() + const { data } = await this.client.permission.list({ directory: workspaceDir }) + if (!data) return + for (const perm of data) { + if (!this.trackedSessionIds.has(perm.sessionID)) continue + this.postMessage({ + type: "permissionRequest", + permission: { + id: perm.id, + sessionID: perm.sessionID, + toolName: perm.permission, + patterns: perm.patterns, + args: perm.metadata, + message: `Permission required: ${perm.permission}`, + tool: perm.tool, + }, + }) + } + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to fetch pending permissions:", error) } } diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index d409aedcc37..49bf33c4d0b 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -169,6 +169,8 @@ export type WebviewMessage = | { type: "todoUpdated"; sessionID: string; items: unknown[] } | { type: "questionRequest"; question: { id: string; sessionID: string; questions: unknown[]; tool?: unknown } } | { type: "questionResolved"; requestID: string } + | { type: "permissionResolved"; permissionID: string } + | { type: "permissionError"; permissionID: string } | { type: "sessionCreated"; session: ReturnType } | { type: "sessionUpdated"; session: ReturnType } | null @@ -228,6 +230,11 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un tool: event.properties.tool, }, } + case "permission.replied": + return { + type: "permissionResolved", + permissionID: event.properties.requestID, + } case "todo.updated": return { type: "todoUpdated", diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts index 32ef47bf193..cc19e449e24 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts @@ -17,6 +17,7 @@ import type { EventMessageUpdated, EventSessionStatus, EventPermissionAsked, + EventPermissionReplied, EventTodoUpdated, EventQuestionAsked, EventQuestionReplied, @@ -302,6 +303,42 @@ describe("mapSSEEventToWebviewMessage", () => { } }) + it("maps permission.replied to permissionResolved", () => { + const event: EventPermissionReplied = { + type: "permission.replied", + properties: { sessionID: "sess-1", requestID: "perm-1", reply: "once" }, + } + const msg = mapSSEEventToWebviewMessage(event, "sess-1") + expect(msg?.type).toBe("permissionResolved") + if (msg?.type === "permissionResolved") { + expect(msg.permissionID).toBe("perm-1") + } + }) + + it("maps permission.replied (always) to permissionResolved", () => { + const event: EventPermissionReplied = { + type: "permission.replied", + properties: { sessionID: "sess-1", requestID: "perm-2", reply: "always" }, + } + const msg = mapSSEEventToWebviewMessage(event, "sess-1") + expect(msg?.type).toBe("permissionResolved") + if (msg?.type === "permissionResolved") { + expect(msg.permissionID).toBe("perm-2") + } + }) + + it("maps permission.replied (reject) to permissionResolved", () => { + const event: EventPermissionReplied = { + type: "permission.replied", + properties: { sessionID: "sess-1", requestID: "perm-3", reply: "reject" }, + } + const msg = mapSSEEventToWebviewMessage(event, "sess-1") + expect(msg?.type).toBe("permissionResolved") + if (msg?.type === "permissionResolved") { + expect(msg.permissionID).toBe("perm-3") + } + }) + it("maps todo.updated to todoUpdated", () => { const event: EventTodoUpdated = { type: "todo.updated", diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 830f93b1a69..e85dd8b0755 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -8,11 +8,16 @@ * tool call rather than in the bottom dock. */ -import { Component, For, Show, createMemo, createSignal } from "solid-js" +import { Component, For, Show, createMemo } from "solid-js" import { Dynamic } from "solid-js/web" import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part" import { Button } from "@kilocode/kilo-ui/button" -import type { AssistantMessage as SDKAssistantMessage, Part as SDKPart, Message as SDKMessage, ToolPart } from "@kilocode/sdk/v2" +import type { + AssistantMessage as SDKAssistantMessage, + Part as SDKPart, + Message as SDKMessage, + ToolPart, +} from "@kilocode/sdk/v2" import { useData } from "@kilocode/kilo-ui/context/data" import { useSession } from "../../context/session" import { useLanguage } from "../../context/language" @@ -101,13 +106,9 @@ export const AssistantMessage: Component = (props) => { // Questions linked to this message (rendered after the last part) const questionForMessage = () => questions().find((q) => q.tool!.messageID === props.message.id) - const [responding, setResponding] = createSignal(false) - const decide = (permissionId: string, response: "once" | "always" | "reject") => { - if (responding()) return - setResponding(true) + if (session.respondingPermissions().has(permissionId)) return session.respondToPermission(permissionId, response) - setResponding(false) } return ( @@ -117,7 +118,8 @@ export const AssistantMessage: Component = (props) => { const perm = () => permissionForPart(part) // Upstream PART_MAPPING["tool"] returns null for todowrite/todoread, // so we detect them here and render via ToolRegistry directly. - const isUpstreamSuppressed = part.type === "tool" && UPSTREAM_SUPPRESSED_TOOLS.has((part as SDKPart & { tool: string }).tool) + const isUpstreamSuppressed = + part.type === "tool" && UPSTREAM_SUPPRESSED_TOOLS.has((part as SDKPart & { tool: string }).tool) return (
@@ -141,49 +143,49 @@ export const AssistantMessage: Component = (props) => { // For other tools: show patterns only if they're meaningful (not just '*') const meaningfulPatterns = p.patterns.filter((pat) => pat !== "*") return ( -
e.stopPropagation()}> - 0}> -
- - {(pattern) => {pattern}} - +
e.stopPropagation()}> + 0}> +
+ + {(pattern) => {pattern}} + +
+
+ +

+ {p.toolName === "todowrite" + ? language.t("settings.permissions.tool.todowrite.description") + : language.t("settings.permissions.tool.todoread.description")} +

+
+
+ + +
- - -

- {p.toolName === "todowrite" - ? language.t("settings.permissions.tool.todowrite.description") - : language.t("settings.permissions.tool.todoread.description")} -

-
-
- - - +

{language.t("ui.permission.sessionHint")}

-

{language.t("ui.permission.sessionHint")}

-
) }} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index a660ebdcca3..bbefc03c335 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -3,7 +3,7 @@ * Main chat container that combines all chat components */ -import { Component, For, Show, createSignal, createEffect, on, onCleanup, onMount } from "solid-js" +import { Component, For, Show, createEffect, on, onCleanup, onMount } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { Icon } from "@kilocode/kilo-ui/icon" import { BasicTool } from "@kilocode/kilo-ui/basic-tool" @@ -54,8 +54,6 @@ export const ChatView: Component = (props) => { }), ) - const [responding, setResponding] = createSignal(false) - onMount(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape" && session.status() === "busy") { @@ -69,10 +67,8 @@ export const ChatView: Component = (props) => { const decide = (response: "once" | "always" | "reject") => { const perm = permissionRequest() - if (!perm || responding()) return - setResponding(true) + if (!perm || session.respondingPermissions().has(perm.id)) return session.respondToPermission(perm.id, response) - setResponding(false) } return ( @@ -111,13 +107,28 @@ export const ChatView: Component = (props) => {
- - -
diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index fb523f07f05..6e626322687 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -92,6 +92,7 @@ interface SessionContextValue { // Pending permission requests permissions: Accessor + respondingPermissions: Accessor> // Pending question requests questions: Accessor @@ -175,6 +176,9 @@ export const SessionProvider: ParentComponent = (props) => { // Pending permissions const [permissions, setPermissions] = createSignal([]) + // Permission IDs that have been responded to but not yet confirmed by the server + const [respondingPermissions, setRespondingPermissions] = createSignal>(new Set()) + // Pending questions const [questions, setQuestions] = createSignal([]) @@ -395,6 +399,14 @@ export const SessionProvider: ParentComponent = (props) => { handlePermissionRequest(message.permission) break + case "permissionResolved": + handlePermissionResolved(message.permissionID) + break + + case "permissionError": + handlePermissionError(message.permissionID) + break + case "todoUpdated": handleTodoUpdated(message.sessionID, message.items) break @@ -601,6 +613,30 @@ export const SessionProvider: ParentComponent = (props) => { setPermissions((prev) => upsertPermission(prev, permission)) } + function handlePermissionResolved(permissionID: string) { + setPermissions((prev) => prev.filter((p) => p.id !== permissionID)) + setRespondingPermissions((prev) => { + if (!prev.has(permissionID)) return prev + const next = new Set(prev) + next.delete(permissionID) + return next + }) + } + + function handlePermissionError(permissionID: string) { + // Remove from responding set so buttons re-enable (permission prompt is still visible) + setRespondingPermissions((prev) => { + if (!prev.has(permissionID)) return prev + const next = new Set(prev) + next.delete(permissionID) + return next + }) + showToast({ + variant: "error", + title: language.t("settings.permissions.toast.updateFailed.title"), + }) + } + function handleQuestionRequest(question: QuestionRequest) { setQuestions((prev) => { const idx = prev.findIndex((q) => q.id === question.id) @@ -904,15 +940,16 @@ export const SessionProvider: ParentComponent = (props) => { const permission = permissions().find((p) => p.id === permissionId) const sessionID = permission?.sessionID ?? currentSessionID() ?? "" + // Mark as responding so the UI disables the buttons. + // The permission is removed when the server confirms via permission.replied SSE. + setRespondingPermissions((prev) => new Set(prev).add(permissionId)) + vscode.postMessage({ type: "permissionResponse", permissionId, sessionID, response, }) - - // Remove from pending permissions - setPermissions((prev) => prev.filter((p) => p.id !== permissionId)) } function clearQuestionError(requestID: string) { @@ -957,6 +994,7 @@ export const SessionProvider: ParentComponent = (props) => { setCloudPreviewId(null) setLoading(false) setPermissions([]) + setRespondingPermissions(new Set()) setQuestions([]) setQuestionErrors(new Set()) setPendingAgentSelection(defaultAgent()) @@ -1097,6 +1135,7 @@ export const SessionProvider: ParentComponent = (props) => { getParts, todos, permissions, + respondingPermissions, questions, questionErrors, selected, diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 2742267e65a..2a7f1b6a9f8 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -386,6 +386,16 @@ export interface PermissionRequestMessage { permission: PermissionRequest } +export interface PermissionResolvedMessage { + type: "permissionResolved" + permissionID: string +} + +export interface PermissionErrorMessage { + type: "permissionError" + permissionID: string +} + export interface TodoUpdatedMessage { type: "todoUpdated" sessionID: string @@ -955,6 +965,8 @@ export type ExtensionMessage = | PartUpdatedMessage | SessionStatusMessage | PermissionRequestMessage + | PermissionResolvedMessage + | PermissionErrorMessage | TodoUpdatedMessage | SessionCreatedMessage | SessionUpdatedMessage From 4f6d421461204fa61e3fe5723f7cd65355778300 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 15:41:57 +0000 Subject: [PATCH 04/17] chore: update kilo-vscode visual regression baselines --- .../composite-webview/bash-with-permission-chromium-linux.png | 4 ++-- .../composite-webview/glob-with-permission-chromium-linux.png | 4 ++-- .../todo-write-with-permission-chromium-linux.png | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png index cb4ea8c6ef7..c05174792c4 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6cbba3354994879f21489539615bbeeb71cb944980c642d502fdab3c4500e73 -size 11000 +oid sha256:bb1d905099640207db9b354e15e5febe0d06dbbd978bb020de15ae1d2d2910cd +size 12581 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png index eb3edfc7392..83f55630f6a 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcb19f99cbd4cbaf403e2224139ff47318c2618457975d4f957b481962031836 -size 9735 +oid sha256:0f62dd07fad581d3a9ce006c06c8e2a9f5f1947a4011e0afdc2e8d08c0c35604 +size 9377 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png index 16210893824..46bcdd75c3a 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9777ee1cb00c3632e71c64d2518f86aab1e4199d52e95b36c0d6344ac042994a -size 14844 +oid sha256:728948fd208b4b5cf6e4171c5dbf3044f700c346682685315bbd962372a13587 +size 15079 From 86bc9a1030c2059d86be48802c8d748be0ec5e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Mon, 9 Mar 2026 12:50:24 -0300 Subject: [PATCH 05/17] fix(vscode): add respondingPermissions to StoryProviders mock session The mockSessionValue in StoryProviders.tsx was missing the respondingPermissions field added in the permission hang fix. Components now call session.respondingPermissions() so the missing field caused a runtime error in stories, breaking the visual regression snapshot for bash-with-permission. --- packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index b7e602de28f..e6548d63ffc 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -132,6 +132,7 @@ export function mockSessionValue(overrides?: { getParts: () => [], todos: () => [], permissions: () => permissions, + respondingPermissions: () => new Set(), questions: () => qs, questionErrors: () => new Set(), selected: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }), From 73c0d924ed993e802ad2721b4f4dfbadacd43ab4 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Tue, 24 Feb 2026 11:49:16 -0500 Subject: [PATCH 06/17] fix(cli): save and load per-agent model selection --- .../src/cli/cmd/tui/context/local.tsx | 5 +- .../test/kilocode/local-model.test.ts | 450 ++++++++++++++++++ 2 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/test/kilocode/local-model.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index d93079f12a4..9fd6bb4f1bc 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -132,6 +132,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } state.pending = false Filesystem.writeJson(filePath, { + model: modelStore.model, // kilocode_change recent: modelStore.recent, favorite: modelStore.favorite, variant: modelStore.variant, @@ -142,6 +143,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ .then((x: any) => { if (Array.isArray(x.recent)) setModelStore("recent", x.recent) if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite) + if (typeof x.model === "object" && x.model !== null) setModelStore("model", x.model) // kilocode_change if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant) }) .catch(() => {}) @@ -241,6 +243,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const val = recent[next] if (!val) return setModelStore("model", agent.current().name, { ...val }) + save() // kilocode_change }, cycleFavorite(direction: 1 | -1) { const favorites = modelStore.favorite.filter((item) => isModelValid(item)) @@ -293,8 +296,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ "recent", uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })), ) - save() } + save() // kilocode_change }) }, toggleFavorite(model: { providerID: string; modelID: string }) { diff --git a/packages/opencode/test/kilocode/local-model.test.ts b/packages/opencode/test/kilocode/local-model.test.ts new file mode 100644 index 00000000000..e0bfed8099d --- /dev/null +++ b/packages/opencode/test/kilocode/local-model.test.ts @@ -0,0 +1,450 @@ +// kilocode_change - new file +// Tests for per-agent model persistence in local.tsx (model.json read/write) +// +// NOTE: Bun test uses solid-js/dist/server.js (SSR build) where createMemo +// evaluates once and never re-evaluates. The @opentui/solid preload plugin +// that swaps server→client build is only in the top-level bunfig preload, +// not the [test] section. Assertions therefore verify persistence via +// model.json file contents rather than model.current() reactive state. + +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { createRoot } from "solid-js" +import path from "path" +import fs from "fs/promises" + +// ── Mutable mock state ────────────────────────────────────────────────────── + +const mockProviders = [ + { + id: "anthropic", + name: "Anthropic", + source: "env" as const, + env: ["ANTHROPIC_API_KEY"], + options: {}, + models: { + "claude-sonnet": { + id: "claude-sonnet", + providerID: "anthropic", + name: "Claude Sonnet", + capabilities: {}, + }, + "claude-opus": { + id: "claude-opus", + providerID: "anthropic", + name: "Claude Opus", + capabilities: {}, + }, + }, + }, +] + +let mockAgents = [ + { name: "code", mode: "primary" as const, hidden: false, model: undefined as any, color: undefined as any, permission: {} }, + { name: "plan", mode: "primary" as const, hidden: false, model: undefined as any, color: undefined as any, permission: {} }, +] + +let mockConfig: { model?: string } = {} +let mockArgs: { model?: string } = {} +let toastMessages: Array<{ variant: string; message: string }> = [] + +// ── Mocks ──────────────────────────────────────────────────────────────────── +// Only mock TUI context modules that are specific to the CLI layer and not +// used by other test files. Do NOT mock widely-used modules like @/global, +// @/provider/provider, or @opentui/core — they persist process-wide in Bun +// and would break other test files. + +let capturedInit: (() => any) | undefined + +mock.module("@tui/context/helper", () => ({ + createSimpleContext: (input: { name: string; init: () => any }) => { + capturedInit = input.init + return { use: () => {}, provider: () => {} } + }, +})) + +mock.module("@tui/context/sync", () => ({ + useSync: () => ({ + data: { + provider: mockProviders, + provider_default: { anthropic: "claude-sonnet" }, + agent: mockAgents, + config: mockConfig, + mcp: {}, + }, + }), +})) + +mock.module("@tui/context/theme", () => ({ + useTheme: () => ({ + theme: { + primary: { buffer: new Float32Array(4) }, + secondary: { buffer: new Float32Array(4) }, + accent: { buffer: new Float32Array(4) }, + success: { buffer: new Float32Array(4) }, + warning: { buffer: new Float32Array(4) }, + error: { buffer: new Float32Array(4) }, + info: { buffer: new Float32Array(4) }, + }, + }), +})) + +mock.module("@tui/context/args", () => ({ + useArgs: () => mockArgs, +})) + +mock.module("@tui/context/sdk", () => ({ + useSDK: () => ({ + client: { + mcp: { + disconnect: async () => {}, + connect: async () => {}, + }, + }, + }), +})) + +const toastMock = { + show: (opts: { variant: string; message: string; duration?: number }) => { + toastMessages.push({ variant: opts.variant, message: opts.message }) + }, +} +mock.module("@tui/ui/toast", () => ({ + useToast: () => toastMock, +})) + +// ── Import under test (after mocks) ──────────────────────────────────────── + +await import("@tui/context/local") + +// Import the real Global to get the state path (set by test preload via XDG_STATE_HOME) +const { Global } = await import("@/global") +const modelJsonPath = path.join(Global.Path.state, "model.json") + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function resetMockState() { + mockAgents = [ + { name: "code", mode: "primary", hidden: false, model: undefined, color: undefined, permission: {} }, + { name: "plan", mode: "primary", hidden: false, model: undefined, color: undefined, permission: {} }, + ] + mockConfig = {} + mockArgs = {} + toastMessages = [] +} + +function runInRoot(): { local: any; dispose: () => void } { + let local: any + let dispose!: () => void + createRoot((d) => { + dispose = d + local = capturedInit!() + }) + return { local: local!, dispose } +} + +async function initLocal(options?: { + prewrite?: Record +}): Promise<{ local: any; dispose: () => void }> { + if (options?.prewrite) { + await fs.writeFile(modelJsonPath, JSON.stringify(options.prewrite)) + } + + if (!capturedInit) throw new Error("capturedInit not set — mock.module for helper failed") + + const { local, dispose } = runInRoot() + + // Poll until model.ready is true + const deadline = Date.now() + 2000 + while (!local.model.ready && Date.now() < deadline) { + await Bun.sleep(10) + } + if (!local.model.ready) throw new Error("model.ready never became true within 2s") + + return { local, dispose } +} + +async function readModelJson(): Promise { + const text = await fs.readFile(modelJsonPath, "utf-8") + return JSON.parse(text) +} + +async function removeModelJson() { + await fs.rm(modelJsonPath, { force: true }).catch(() => {}) +} + +const SONNET = { providerID: "anthropic", modelID: "claude-sonnet" } +const OPUS = { providerID: "anthropic", modelID: "claude-opus" } + +// ── Setup ─────────────────────────────────────────────────────────────────── + +beforeEach(async () => { + resetMockState() + await removeModelJson() +}) + +afterEach(async () => { + await removeModelJson() +}) + +// ── Tests ─────────────────────────────────────────────────────────────────── + +describe("model.set persists per-agent model", () => { + test("1: model.set for agent 'code' writes model.code to model.json", async () => { + const { local, dispose } = await initLocal() + try { + local.model.set(OPUS, { recent: true }) + await Bun.sleep(50) + + const data = await readModelJson() + expect(data.model.code).toEqual(OPUS) + expect(data.recent).toBeArrayOfSize(1) + expect(data.recent[0]).toEqual(OPUS) + } finally { + dispose() + } + }) + + test("2: set code→sonnet, plan→opus persists both in model.json", async () => { + const { local, dispose } = await initLocal() + try { + local.model.set(SONNET, { recent: true }) + local.agent.set("plan") + local.model.set(OPUS, { recent: true }) + await Bun.sleep(50) + + const data = await readModelJson() + expect(data.model.code).toEqual(SONNET) + expect(data.model.plan).toEqual(OPUS) + } finally { + dispose() + } + }) + + test("3: save → dispose → re-init from same dir loads persisted data", async () => { + // First session: set and save + { + const { local, dispose } = runInRoot() + const deadline = Date.now() + 2000 + while (!local.model.ready && Date.now() < deadline) await Bun.sleep(10) + local.model.set(OPUS, { recent: true }) + await Bun.sleep(50) + dispose() + } + + // Verify file was written correctly + const fileData = await readModelJson() + expect(fileData.model.code).toEqual(OPUS) + expect(fileData.recent[0]).toEqual(OPUS) + + // Second session: re-init from same dir — verify file data was loaded + { + const { local, dispose } = runInRoot() + const deadline = Date.now() + 2000 + while (!local.model.ready && Date.now() < deadline) await Bun.sleep(10) + + // recent() reads the store directly (not a memo), so it reflects the loaded file + expect(local.model.recent()).toEqual([OPUS]) + + // Setting a new model on top of loaded data should produce correct file + local.model.set(SONNET, { recent: true }) + await Bun.sleep(50) + const data2 = await readModelJson() + expect(data2.model.code).toEqual(SONNET) + expect(data2.recent[0]).toEqual(SONNET) + expect(data2.recent[1]).toEqual(OPUS) + dispose() + } + }) +}) + +describe("model.cycle and model.cycleFavorite", () => { + test("4: cycle(1) advances to next recent model, persists per-agent", async () => { + const { local, dispose } = await initLocal({ + prewrite: { + recent: [SONNET, OPUS], + model: { code: SONNET }, + favorite: [], + variant: {}, + }, + }) + try { + local.model.cycle(1) + await Bun.sleep(50) + + const data = await readModelJson() + expect(data.model.code).toEqual(OPUS) + } finally { + dispose() + } + }) + + test("5: cycleFavorite(1) cycles to next favorite and persists", async () => { + const { local, dispose } = await initLocal({ + prewrite: { + recent: [SONNET], + model: { code: SONNET }, + favorite: [SONNET, OPUS], + variant: {}, + }, + }) + try { + local.model.cycleFavorite(1) + await Bun.sleep(50) + + const data = await readModelJson() + expect(data.model.code).toEqual(OPUS) + expect(data.recent.some((r: any) => r.modelID === "claude-opus")).toBe(true) + } finally { + dispose() + } + }) +}) + +describe("edge cases and error handling", () => { + test("6: model.json without 'model' field does not crash, falls back", async () => { + const { local, dispose } = await initLocal({ + prewrite: { + recent: [SONNET], + favorite: [], + variant: {}, + }, + }) + try { + expect(local.model.ready).toBe(true) + // recent() reads the store directly and should have loaded + expect(local.model.recent()).toEqual([SONNET]) + // current() evaluates once at init before file loads — falls to provider default + expect(local.model.current()).toBeDefined() + } finally { + dispose() + } + }) + + test("7: corrupt model.json does not crash, defaults to empty state", async () => { + await fs.writeFile(modelJsonPath, "{{{not valid json") + + const { local, dispose } = runInRoot() + const deadline = Date.now() + 2000 + while (!local.model.ready && Date.now() < deadline) await Bun.sleep(10) + + try { + expect(local.model.ready).toBe(true) + // Should not have loaded any data + expect(local.model.recent()).toEqual([]) + expect(local.model.favorite()).toEqual([]) + } finally { + dispose() + } + }) + + test("8: pre-written model.code with invalid provider — valid models still load", async () => { + const { local, dispose } = await initLocal({ + prewrite: { + recent: [SONNET], + model: { code: { providerID: "nonexistent", modelID: "fake-model" } }, + favorite: [], + variant: {}, + }, + }) + try { + // recent loaded correctly despite invalid model.code + expect(local.model.recent()).toEqual([SONNET]) + // The initial current() evaluates before file loads, so it falls to default + const current = local.model.current() + expect(current).toBeDefined() + expect(current!.providerID).toBe("anthropic") + } finally { + dispose() + } + }) + + test("9: model.set before ready still persists after ready", async () => { + const { local, dispose } = runInRoot() + + // Immediately set before ready (ready is false because file load is async) + const wasReadyBefore = local.model.ready + local.model.set(OPUS, { recent: true }) + + // Wait for ready + const deadline = Date.now() + 2000 + while (!local.model.ready && Date.now() < deadline) await Bun.sleep(10) + await Bun.sleep(50) + + try { + expect(wasReadyBefore).toBe(false) + expect(local.model.ready).toBe(true) + const data = await readModelJson() + expect(data.model.code).toEqual(OPUS) + } finally { + dispose() + } + }) + + test("10: agent with config model persists when applied", async () => { + // NOTE: In production, a createEffect in local.tsx auto-applies agent config + // models when switching agents. In bun test, createEffect is a no-op (SSR build). + // This test verifies the underlying persistence: when an agent has a config model, + // model.set (what the effect would call) correctly persists it. + mockAgents = [ + { name: "code", mode: "primary", hidden: false, model: undefined, color: undefined, permission: {} }, + { name: "plan", mode: "primary", hidden: false, model: OPUS, color: undefined, permission: {} }, + ] + const { local, dispose } = await initLocal() + try { + // Switch to "plan" agent which has config model OPUS + local.agent.set("plan") + // Simulate what createEffect would do: apply the agent's config model + local.model.set(OPUS) + await Bun.sleep(50) + + const data = await readModelJson() + expect(data.model.plan).toEqual(OPUS) + } finally { + dispose() + } + }) + + test("11: user override from file is retained after load", async () => { + mockAgents = [ + { name: "code", mode: "primary", hidden: false, model: OPUS, color: undefined, permission: {} }, + { name: "plan", mode: "primary", hidden: false, model: undefined, color: undefined, permission: {} }, + ] + const { local, dispose } = await initLocal({ + prewrite: { + recent: [SONNET, OPUS], + model: { code: SONNET }, + favorite: [], + variant: {}, + }, + }) + try { + // The file had model.code = SONNET, recent = [SONNET, OPUS] + // Verify file data was loaded into the store + expect(local.model.recent()).toEqual([SONNET, OPUS]) + + // Setting a new model should layer on top of the loaded state + local.model.set(OPUS, { recent: true }) + await Bun.sleep(50) + const data = await readModelJson() + // model.code should now be OPUS (the new set) + expect(data.model.code).toEqual(OPUS) + } finally { + dispose() + } + }) + + test("12: switching agents without config models produces no warning toasts", async () => { + const { local, dispose } = await initLocal() + try { + toastMessages = [] + local.agent.set("plan") + local.agent.set("code") + await Bun.sleep(50) + + const warnings = toastMessages.filter((t) => t.message.includes("configured model")) + expect(warnings).toHaveLength(0) + } finally { + dispose() + } + }) +}) From d1409ff2576796069a3dbe169f9db68b79847a17 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Mon, 9 Mar 2026 17:16:13 +0100 Subject: [PATCH 07/17] feat: always show prompt input during permission prompts --- .../kilo-vscode/webview-ui/src/components/chat/ChatView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index a660ebdcca3..8df127d1427 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -151,9 +151,7 @@ export const ChatView: Component = (props) => {
- - - +
From 2f66f96533d774977f76049262c4402049bc2db2 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Mon, 9 Mar 2026 17:18:33 +0100 Subject: [PATCH 08/17] fix: hide working indicator when waiting for user input --- .../src/components/shared/WorkingIndicator.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/WorkingIndicator.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/WorkingIndicator.tsx index 05eb47b65bb..6d35fed0344 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/WorkingIndicator.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/WorkingIndicator.tsx @@ -71,8 +71,17 @@ export const WorkingIndicator: Component = () => { return `${m}m ${rem}s` } + const blocked = () => { + const id = session.currentSessionID() + const perms = session + .permissions() + .filter((p) => p.sessionID === id && !(p.tool && ["todowrite", "todoread"].includes(p.toolName))) + const questions = session.questions().filter((q) => q.sessionID === id) + return perms.length > 0 || questions.length > 0 + } + return ( - +
{statusText()} From 4b4fa6e90185fa1b21c83965d653c027d1b17839 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 16:45:09 +0000 Subject: [PATCH 09/17] chore: update kilo-vscode visual regression baselines --- .../composite-webview/bash-with-permission-chromium-linux.png | 4 ++-- .../composite-webview/glob-with-permission-chromium-linux.png | 4 ++-- .../todo-write-with-permission-chromium-linux.png | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png index c05174792c4..cb4ea8c6ef7 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/bash-with-permission-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb1d905099640207db9b354e15e5febe0d06dbbd978bb020de15ae1d2d2910cd -size 12581 +oid sha256:b6cbba3354994879f21489539615bbeeb71cb944980c642d502fdab3c4500e73 +size 11000 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png index 83f55630f6a..eb3edfc7392 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/glob-with-permission-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f62dd07fad581d3a9ce006c06c8e2a9f5f1947a4011e0afdc2e8d08c0c35604 -size 9377 +oid sha256:bcb19f99cbd4cbaf403e2224139ff47318c2618457975d4f957b481962031836 +size 9735 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png index 46bcdd75c3a..16210893824 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/composite-webview/todo-write-with-permission-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:728948fd208b4b5cf6e4171c5dbf3044f700c346682685315bbd962372a13587 -size 15079 +oid sha256:9777ee1cb00c3632e71c64d2518f86aab1e4199d52e95b36c0d6344ac042994a +size 14844 From 27ab1bef61cddb39957d4fa65c011b075a649fae Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 9 Mar 2026 17:53:20 +0100 Subject: [PATCH 10/17] fix: use arrow-down-to-line SVG icon for scroll-to-bottom button (#6810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace plain Unicode ↓ character with the proper component from kilo-ui, matching the desktop app's implementation. --- .../kilo-vscode/webview-ui/src/components/chat/MessageList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 6764da91916..a2412caaa36 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -8,6 +8,7 @@ */ import { Component, For, Show, createEffect, createMemo, onCleanup, JSX } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" import { Spinner } from "@kilocode/kilo-ui/spinner" import { Button } from "@kilocode/kilo-ui/button" import { useDialog } from "@kilocode/kilo-ui/context/dialog" @@ -144,7 +145,7 @@ export const MessageList: Component = (props) => { onClick={() => autoScroll.resume()} aria-label={language.t("session.messages.scrollToBottom")} > - ↓ +
From 593b6f97c177de9400390545bc6eb5efed1c10b9 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:52:12 +0000 Subject: [PATCH 11/17] docs: add AWS Bedrock as a supported BYOK provider Add Bedrock to the list of supported BYOK providers and document the AWS-specific credential format (JSON with accessKeyId, secretAccessKey, and region) and required IAM permissions. --- .../kilo-docs/pages/getting-started/byok.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index 2dd60538750..3df9aff13e1 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -19,6 +19,7 @@ A user or organization may want to use BYOK to: Kilo Gateway currently supports BYOK keys for these providers: - Anthropic +- AWS Bedrock - OpenAI - Google AI Studio - Minimax @@ -33,6 +34,29 @@ Kilo Gateway currently supports BYOK keys for these providers: 3. Click `Add Your First Key`, select the provider, and paste your API key. 4. Save. +### AWS Bedrock configuration + +AWS Bedrock requires credentials in a different format than other providers. Instead of a single API key, you must provide your AWS credentials as a JSON object: + +```json +{ + "accessKeyId": "AKIA...", + "secretAccessKey": "...", + "region": "us-east-1" +} +``` + +| Field | Description | +| ----------------- | ------------------------------------------------------------------------ | +| `accessKeyId` | Your AWS access key ID | +| `secretAccessKey` | Your AWS secret access key | +| `region` | The AWS region where Bedrock is enabled (e.g., `us-east-1`, `eu-west-1`) | + +Your IAM user or role must have the following permissions: + +- `bedrock:InvokeModel` +- `bedrock:InvokeModelWithResponseStream` + ## How Bring Your Own Key works - When you use the **Kilo Gateway** provider, Kilo checks if there's a BYOK key for the selected model's provider. From 25b2c5ae7ef9ea32f68f377928a1f4e20b11a8fc Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Fri, 6 Mar 2026 13:32:03 -0500 Subject: [PATCH 12/17] feat: redesign Auto-Approve settings to support granular permission rules Redesign the Auto-Approve tab in VS Code extension settings to display and edit granular pattern-based permission rules (object syntax). - Add PermissionRule type to support nested objects (e.g. { bash: { "*": "deny", "git *": "allow" } }) - Granular tools (external_directory, bash, read, edit) show wildcard row, exceptions list with per-pattern dropdowns, delete buttons, and inline add input - Simple tools show single Ask/Allow/Deny dropdown - Group todoread/todowrite and websearch/codesearch into combined rows - Remove "Set all permissions" card, add description subtitle - Truncate long patterns with ellipsis and title tooltip - Two-phase config update for exception deletion to work with patchJsonc --- .../components/settings/AutoApproveTab.tsx | 547 ++++++++++++++---- .../kilo-vscode/webview-ui/src/i18n/en.ts | 46 +- .../webview-ui/src/types/messages.ts | 4 +- 3 files changed, 479 insertions(+), 118 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx index 79b08ed6554..5ec66b77da4 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx @@ -1,28 +1,9 @@ -import { Component, For, createMemo } from "solid-js" +import { Component, For, Show, createMemo, createSignal } from "solid-js" import { Select } from "@kilocode/kilo-ui/select" -import { Card } from "@kilocode/kilo-ui/card" +import { IconButton } from "@kilocode/kilo-ui/icon-button" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" -import type { PermissionLevel } from "../../types/messages" - -const TOOLS = [ - "read", - "edit", - "glob", - "grep", - "list", - "bash", - "task", - "skill", - "lsp", - "todoread", - "todowrite", - "webfetch", - "websearch", - "codesearch", - "external_directory", - "doom_loop", -] as const +import type { PermissionLevel, PermissionRule } from "../../types/messages" interface LevelOption { value: PermissionLevel @@ -35,104 +16,470 @@ const LEVEL_OPTIONS: LevelOption[] = [ { value: "deny", labelKey: "settings.autoApprove.level.deny" }, ] +interface ToolDef { + id: string + descriptionKey: string + granular?: { + wildcardKey: string + addKey: string + placeholderKey: string + } +} + +/** Grouped tool: maps a single UI row to multiple config keys */ +interface GroupedToolDef { + ids: string[] + label: string + descriptionKey: string +} + +const GRANULAR_TOOLS: ToolDef[] = [ + { + id: "external_directory", + descriptionKey: "settings.autoApprove.tool.external_directory", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.paths", + addKey: "settings.autoApprove.addPath", + placeholderKey: "settings.autoApprove.placeholder.path", + }, + }, + { + id: "bash", + descriptionKey: "settings.autoApprove.tool.bash", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.commands", + addKey: "settings.autoApprove.addCommand", + placeholderKey: "settings.autoApprove.placeholder.command", + }, + }, + { + id: "read", + descriptionKey: "settings.autoApprove.tool.read", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.paths", + addKey: "settings.autoApprove.addPath", + placeholderKey: "settings.autoApprove.placeholder.path", + }, + }, + { + id: "edit", + descriptionKey: "settings.autoApprove.tool.edit", + granular: { + wildcardKey: "settings.autoApprove.wildcardLabel.paths", + addKey: "settings.autoApprove.addPath", + placeholderKey: "settings.autoApprove.placeholder.path", + }, + }, +] + +const SIMPLE_TOOLS: ToolDef[] = [ + { id: "glob", descriptionKey: "settings.autoApprove.tool.glob" }, + { id: "grep", descriptionKey: "settings.autoApprove.tool.grep" }, + { id: "list", descriptionKey: "settings.autoApprove.tool.list" }, + { id: "task", descriptionKey: "settings.autoApprove.tool.task" }, + { id: "skill", descriptionKey: "settings.autoApprove.tool.skill" }, + { id: "lsp", descriptionKey: "settings.autoApprove.tool.lsp" }, +] + +const GROUPED_TOOLS: GroupedToolDef[] = [ + { + ids: ["todoread", "todowrite"], + label: "todoread / todowrite", + descriptionKey: "settings.autoApprove.tool.todoreadwrite", + }, + { + ids: ["websearch", "codesearch"], + label: "websearch / codesearch", + descriptionKey: "settings.autoApprove.tool.websearchcodesearch", + }, +] + +const TRAILING_TOOLS: ToolDef[] = [ + { id: "webfetch", descriptionKey: "settings.autoApprove.tool.webfetch" }, + { id: "doom_loop", descriptionKey: "settings.autoApprove.tool.doom_loop" }, +] + +function wildcardAction(rule: PermissionRule | undefined, fallback: PermissionLevel): PermissionLevel { + if (!rule) return fallback + if (typeof rule === "string") return rule + return rule["*"] ?? fallback +} + +function exceptions(rule: PermissionRule | undefined): Array<{ pattern: string; action: PermissionLevel }> { + if (!rule || typeof rule === "string") return [] + return Object.entries(rule) + .filter(([key]) => key !== "*") + .map(([pattern, action]) => ({ pattern, action })) +} + const AutoApproveTab: Component = () => { const { config, updateConfig } = useConfig() const language = useLanguage() const permissions = createMemo(() => config().permission ?? {}) - const getLevel = (tool: string): PermissionLevel => { - return permissions()[tool] ?? permissions()["*"] ?? "ask" + const globalFallback = createMemo((): PermissionLevel => { + const star = permissions()["*"] + if (typeof star === "string") return star + return "ask" + }) + + const levelFor = (tool: string): PermissionLevel => wildcardAction(permissions()[tool], globalFallback()) + + const ruleFor = (tool: string): PermissionRule | undefined => permissions()[tool] + + const setSimple = (tool: string, level: PermissionLevel) => { + updateConfig({ permission: { [tool]: level } }) } - const setPermission = (tool: string, level: PermissionLevel) => { - updateConfig({ - permission: { ...permissions(), [tool]: level }, - }) + const setGrouped = (ids: string[], level: PermissionLevel) => { + const patch: Record = {} + for (const id of ids) patch[id] = level + updateConfig({ permission: patch }) } - const setAll = (level: PermissionLevel) => { - const updated: Record = {} - for (const tool of TOOLS) { - updated[tool] = level + const setWildcard = (tool: string, level: PermissionLevel) => { + const current = ruleFor(tool) + const excs = exceptions(current) + if (excs.length === 0) { + updateConfig({ permission: { [tool]: level } }) + return + } + const obj: Record = { "*": level } + for (const exc of excs) obj[exc.pattern] = exc.action + updateConfig({ permission: { [tool]: obj } }) + } + + const setException = (tool: string, pattern: string, level: PermissionLevel) => { + const current = ruleFor(tool) + const base: Record = + typeof current === "string" ? { "*": current } : { ...(current ?? {}) } + base[pattern] = level + updateConfig({ permission: { [tool]: base } }) + } + + const addException = (tool: string, pattern: string) => { + const current = ruleFor(tool) + const base: Record = + typeof current === "string" ? { "*": current } : { ...(current ?? {}) } + base[pattern] = "allow" + updateConfig({ permission: { [tool]: base } }) + } + + const removeException = (tool: string, pattern: string) => { + const current = ruleFor(tool) + if (!current || typeof current === "string") return + const rebuilt: Record = {} + for (const [k, v] of Object.entries(current)) { + if (k !== pattern) rebuilt[k] = v + } + const keys = Object.keys(rebuilt) + const value: PermissionRule = + keys.length === 0 ? "ask" : keys.length === 1 && keys[0] === "*" ? rebuilt["*"]! : rebuilt + // patchJsonc only sets keys present in the patch — it won't remove the deleted key + // from the JSONC file. To work around this, first set the tool to a string (which + // replaces the entire JSONC node), then set the rebuilt object if needed. + const wildcard = rebuilt["*"] ?? "ask" + updateConfig({ permission: { [tool]: wildcard } }) + if (typeof value === "object") { + updateConfig({ permission: { [tool]: value } }) } - updateConfig({ permission: updated }) } return (
- {/* Set All control */} - -
- {language.t("settings.autoApprove.setAll")} - o.value === getLevel(tool))} - value={(o) => o.value} - label={(o) => language.t(o.labelKey)} - onSelect={(option) => option && setPermission(tool, option.value)} - variant="secondary" - size="small" - triggerVariant="settings" - /> -
- )} - -
+ + {(group) => ( + setGrouped(group.ids, level)} + /> + )} + + + + {(tool) => ( + setSimple(tool.id, level)} + /> + )} +
) } +const SimpleToolRow: Component<{ + id: string + descriptionKey: string + level: PermissionLevel + onChange: (level: PermissionLevel) => void +}> = (props) => { + const language = useLanguage() + return ( +
+
+
{props.id}
+
+ {language.t(props.descriptionKey)} +
+
+ +
+ ) +} + +const GranularToolRow: Component<{ + tool: ToolDef + rule: PermissionRule | undefined + fallback: PermissionLevel + onWildcardChange: (level: PermissionLevel) => void + onExceptionChange: (pattern: string, level: PermissionLevel) => void + onExceptionAdd: (pattern: string) => void + onExceptionRemove: (pattern: string) => void +}> = (props) => { + const language = useLanguage() + const [adding, setAdding] = createSignal(false) + const [input, setInput] = createSignal("") + + const excs = createMemo(() => exceptions(props.rule)) + const level = createMemo(() => wildcardAction(props.rule, props.fallback)) + + const submit = () => { + const val = input().trim() + if (val) { + props.onExceptionAdd(val) + setInput("") + } + setAdding(false) + } + + const cancel = () => { + setInput("") + setAdding(false) + } + + return ( +
+ {/* Tool header with name and description */} +
+
+
{props.tool.id}
+
+ {language.t(props.tool.descriptionKey)} +
+
+
+ + {/* Wildcard row */} +
+
+
+ {language.t(props.tool.granular!.wildcardKey)} +
+
+ +
+ + {/* Exceptions */} + 0}> +
+
+ {language.t("settings.autoApprove.exceptions")} +
+ + {(exc) => ( +
+
+ {exc.pattern} +
+
+ props.onExceptionChange(exc.pattern, level)} /> + props.onExceptionRemove(exc.pattern)} + /> +
+
+ )} +
+
+
+ + {/* Add button / inline input */} + setAdding(true)} + > + + + {language.t(props.tool.granular!.addKey)} + + } + > +
+ setTimeout(() => el.focus(), 0)} + type="text" + value={input()} + onInput={(e) => setInput(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") submit() + if (e.key === "Escape") cancel() + }} + onBlur={() => { + if (!input().trim()) cancel() + }} + placeholder={language.t(props.tool.granular!.placeholderKey)} + style={{ + flex: 1, + "min-width": 0, + background: "var(--surface-strong-base, #252526)", + border: "1px solid var(--border-base, #434443)", + "border-radius": "2px", + color: "var(--text-base, #ccc)", + "font-size": "13px", + "font-family": "var(--vscode-editor-font-family, monospace)", + padding: "4px 8px", + outline: "none", + }} + /> + +
+
+
+ ) +} + +const ActionSelect: Component<{ + level: PermissionLevel + onChange: (level: PermissionLevel) => void +}> = (props) => { + const language = useLanguage() + return ( + setTimeout(() => el.focus(), 0)} + ref={(el) => (inputRef = el)} type="text" value={input()} onInput={(e) => setInput(e.currentTarget.value)} @@ -470,7 +499,7 @@ const GranularToolRow: Component<{ onBlur={() => { if (!input().trim()) cancel() }} - placeholder={language.t(props.tool.granular!.placeholderKey)} + placeholder={language.t(props.tool.granular.placeholderKey)} style={{ flex: 1, "min-width": 0, diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 1e4ac26eeec..4812422e1d2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -947,21 +947,21 @@ export const dict = { "settings.autoApprove.addPath": "إضافة مسار", "settings.autoApprove.placeholder.command": "مثال: git *", "settings.autoApprove.placeholder.path": "مثال: *.env", - "settings.autoApprove.tool.read": "قراءة محتويات الملفات", - "settings.autoApprove.tool.edit": "تحرير أو إنشاء الملفات", - "settings.autoApprove.tool.glob": "البحث عن ملفات بنمط", - "settings.autoApprove.tool.grep": "البحث في محتويات الملفات", - "settings.autoApprove.tool.list": "عرض محتويات المجلد", - "settings.autoApprove.tool.bash": "تنفيذ أوامر الصدفة", - "settings.autoApprove.tool.task": "إنشاء مهام وكيل فرعي", - "settings.autoApprove.tool.skill": "تنفيذ المهارات", - "settings.autoApprove.tool.lsp": "عمليات خادم اللغة", + "settings.autoApprove.tool.read": "قراءة الملفات. يسمح للوكيل بقراءة الملفات التي تطابق المسار المحدد.", + "settings.autoApprove.tool.edit": "تعديل الملفات. يسمح للوكيل بإنشاء أو تحرير الملفات، بما في ذلك التصحيحات والتحديثات متعددة الملفات.", + "settings.autoApprove.tool.glob": "مطابقة الملفات بالنمط. يسمح بمطابقة الملفات باستخدام أنماط glob (مثل src/**/*.ts).", + "settings.autoApprove.tool.grep": "البحث في محتويات الملفات. يسمح بالبحث المستند إلى التعبيرات النمطية (regex) داخل الملفات.", + "settings.autoApprove.tool.list": "عرض محتويات المجلد. يسمح بعرض الملفات والمجلدات داخل الدليل.", + "settings.autoApprove.tool.bash": "تنفيذ أوامر الطرفية. يسمح بتنفيذ أوامر الصدفة (مثل git status).", + "settings.autoApprove.tool.task": "إطلاق وكلاء فرعيين. يسمح ببدء وكلاء فرعيين متخصصين لمهام محددة.", + "settings.autoApprove.tool.skill": "تحميل المهارات. يسمح بتحميل المهارات المحددة مسبقًا بالاسم.", + "settings.autoApprove.tool.lsp": "الاستعلام من خادم اللغة. يسمح بتشغيل استعلامات خادم اللغة لذكاء الكود.", "settings.autoApprove.tool.todoreadwrite": "إدارة قائمة المهام. يسمح بقراءة وتحديث قائمة المهام الداخلية.", - "settings.autoApprove.tool.webfetch": "جلب صفحات الويب", + "settings.autoApprove.tool.webfetch": "جلب رابط ويب. يسمح باسترداد المحتوى من رابط (URL) محدد.", "settings.autoApprove.tool.websearchcodesearch": "البحث في الويب أو الكود. يسمح بإجراء عمليات بحث خارجية في الويب أو الكود.", - "settings.autoApprove.tool.external_directory": "الوصول للملفات خارج مساحة العمل", - "settings.autoApprove.tool.doom_loop": "المتابعة بعد الإخفاقات المتكررة", + "settings.autoApprove.tool.external_directory": "الوصول للملفات خارج مساحة العمل. يتم تشغيله عند الوصول إلى ملفات خارج مسار المشروع الحالي.", + "settings.autoApprove.tool.doom_loop": "منع الإجراءات المتطابقة المتكررة. يتم تشغيله عندما يتكرر نفس استدعاء الأداة بمدخلات متطابقة.", "settings.checkpoints.enable.title": "تمكين اللقطات", "settings.checkpoints.enable.description": "إنشاء نقاط فحص قبل تحرير الملفات", "settings.context.autoCompaction.title": "ضغط تلقائي", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 1f0cc3e9cec..43a471b2ab9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -961,22 +961,22 @@ export const dict = { "settings.autoApprove.addPath": "Adicionar caminho", "settings.autoApprove.placeholder.command": "ex. git *", "settings.autoApprove.placeholder.path": "ex. *.env", - "settings.autoApprove.tool.read": "Ler conteúdo de arquivos", - "settings.autoApprove.tool.edit": "Editar ou criar arquivos", - "settings.autoApprove.tool.glob": "Encontrar arquivos por padrão", - "settings.autoApprove.tool.grep": "Pesquisar conteúdo de arquivos", - "settings.autoApprove.tool.list": "Listar conteúdo do diretório", - "settings.autoApprove.tool.bash": "Executar comandos shell", - "settings.autoApprove.tool.task": "Criar tarefas de sub-agente", - "settings.autoApprove.tool.skill": "Executar habilidades", - "settings.autoApprove.tool.lsp": "Operações do servidor de linguagem", + "settings.autoApprove.tool.read": "Ler arquivos. Permite que o agente leia arquivos correspondentes ao caminho especificado.", + "settings.autoApprove.tool.edit": "Modificar arquivos. Permite que o agente crie ou edite arquivos, incluindo patches e atualizações de múltiplos arquivos.", + "settings.autoApprove.tool.glob": "Corresponder arquivos por padrão. Permite a correspondência de arquivos usando padrões glob (ex: src/**/*.ts).", + "settings.autoApprove.tool.grep": "Pesquisar conteúdo de arquivos. Permite a pesquisa baseada em regex dentro de arquivos.", + "settings.autoApprove.tool.list": "Listar conteúdo de diretórios. Permite visualizar arquivos e pastas dentro de um diretório.", + "settings.autoApprove.tool.bash": "Executar comandos de terminal. Permite a execução de comandos shell (ex: git status).", + "settings.autoApprove.tool.task": "Iniciar subagentes. Permite iniciar subagentes especializados para tarefas específicas.", + "settings.autoApprove.tool.skill": "Carregar habilidades. Permite carregar habilidades predefinidas pelo nome.", + "settings.autoApprove.tool.lsp": "Consultar language server. Permite executar consultas ao servidor de linguagem para inteligência de código.", "settings.autoApprove.tool.todoreadwrite": "Gerenciar lista de tarefas. Permite ler e atualizar a lista de tarefas interna.", - "settings.autoApprove.tool.webfetch": "Buscar páginas web", + "settings.autoApprove.tool.webfetch": "Buscar uma URL. Permite recuperar o conteúdo de uma URL específica.", "settings.autoApprove.tool.websearchcodesearch": "Pesquisar na web ou no código. Permite realizar pesquisas externas na web ou no código.", - "settings.autoApprove.tool.external_directory": "Acessar arquivos fora do espaço de trabalho", - "settings.autoApprove.tool.doom_loop": "Continuar após falhas repetidas", + "settings.autoApprove.tool.external_directory": "Acessar arquivos fora do workspace. Acionado ao acessar arquivos fora do diretório do projeto atual.", + "settings.autoApprove.tool.doom_loop": "Prevenir ações idênticas repetidas. Acionado quando a mesma chamada de ferramenta se repete com entrada idêntica.", "settings.checkpoints.enable.title": "Ativar snapshots", "settings.checkpoints.enable.description": "Criar pontos de verificação antes de editar arquivos", "settings.context.autoCompaction.title": "Compactação automática", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index e1346a8bd4c..699641aa0e5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -960,22 +960,22 @@ export const dict = { "settings.autoApprove.addPath": "Dodaj putanju", "settings.autoApprove.placeholder.command": "npr. git *", "settings.autoApprove.placeholder.path": "npr. *.env", - "settings.autoApprove.tool.read": "Čitanje sadržaja datoteka", - "settings.autoApprove.tool.edit": "Uređivanje ili kreiranje datoteka", - "settings.autoApprove.tool.glob": "Pronalaženje datoteka po uzorku", - "settings.autoApprove.tool.grep": "Pretraživanje sadržaja datoteka", - "settings.autoApprove.tool.list": "Prikaz sadržaja direktorija", - "settings.autoApprove.tool.bash": "Pokretanje shell naredbi", - "settings.autoApprove.tool.task": "Kreiranje podzadataka agenta", - "settings.autoApprove.tool.skill": "Pokretanje vještina", - "settings.autoApprove.tool.lsp": "Operacije jezičkog servera", + "settings.autoApprove.tool.read": "Čitanje datoteka. Omogućava agentu da čita datoteke koje odgovaraju navedenoj putanji.", + "settings.autoApprove.tool.edit": "Izmjena datoteka. Omogućava agentu da kreira ili uređuje datoteke, uključujući zakrpe i ažuriranja više datoteka.", + "settings.autoApprove.tool.glob": "Podudaranje datoteka po uzorku. Omogućava podudaranje datoteka koristeći glob uzorke (npr. src/**/*.ts).", + "settings.autoApprove.tool.grep": "Pretraživanje sadržaja datoteka. Omogućava pretraživanje zasnovano na regex-u unutar datoteka.", + "settings.autoApprove.tool.list": "Prikaz sadržaja direktorija. Omogućava pregled datoteka i fascikli unutar direktorija.", + "settings.autoApprove.tool.bash": "Pokretanje terminalskih komandi. Omogućava izvršavanje shell komandi (npr. git status).", + "settings.autoApprove.tool.task": "Pokretanje pod-agenata. Omogućava pokretanje specijalizovanih pod-agenata za određene zadatke.", + "settings.autoApprove.tool.skill": "Učitavanje vještina. Omogućava učitavanje unaprijed definisanih vještina po imenu.", + "settings.autoApprove.tool.lsp": "Upit ka jezičkom serveru. Omogućava pokretanje upita jezičkom serveru za analizu koda.", "settings.autoApprove.tool.todoreadwrite": "Upravljanje listom zadataka. Dozvoljava čitanje i ažuriranje interne liste zadataka.", - "settings.autoApprove.tool.webfetch": "Dohvaćanje web stranica", + "settings.autoApprove.tool.webfetch": "Preuzimanje URL-a. Omogućava preuzimanje sadržaja sa određenog URL-a.", "settings.autoApprove.tool.websearchcodesearch": "Pretraga weba ili koda. Dozvoljava izvršavanje eksternih pretraga weba ili koda.", - "settings.autoApprove.tool.external_directory": "Pristup datotekama izvan radnog prostora", - "settings.autoApprove.tool.doom_loop": "Nastavi nakon ponovljenih neuspjeha", + "settings.autoApprove.tool.external_directory": "Pristup datotekama izvan radnog prostora. Pokreće se prilikom pristupa datotekama izvan trenutnog direktorija projekta.", + "settings.autoApprove.tool.doom_loop": "Sprečavanje ponavljanja identičnih radnji. Pokreće se kada se isti poziv alata ponovi sa identičnim unosom.", "settings.checkpoints.enable.title": "Omogući snimke", "settings.checkpoints.enable.description": "Kreiraj kontrolne točke prije uređivanja datoteka", "settings.context.autoCompaction.title": "Automatska kompresija", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 0487b0e18ca..4b09d616e92 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -956,22 +956,22 @@ export const dict = { "settings.autoApprove.addPath": "Tilføj sti", "settings.autoApprove.placeholder.command": "f.eks. git *", "settings.autoApprove.placeholder.path": "f.eks. *.env", - "settings.autoApprove.tool.read": "Læs filindhold", - "settings.autoApprove.tool.edit": "Rediger eller opret filer", - "settings.autoApprove.tool.glob": "Find filer efter mønster", - "settings.autoApprove.tool.grep": "Søg i filindhold", - "settings.autoApprove.tool.list": "List mappeindhold", - "settings.autoApprove.tool.bash": "Udfør shell-kommandoer", - "settings.autoApprove.tool.task": "Opret underagentopgaver", - "settings.autoApprove.tool.skill": "Udfør skills", - "settings.autoApprove.tool.lsp": "Sprogserveroperationer", + "settings.autoApprove.tool.read": "Læs filer. Tillader agenten at læse filer, der matcher den angivne sti.", + "settings.autoApprove.tool.edit": "Rediger filer. Tillader agenten at oprette eller redigere filer, herunder patches og opdateringer af flere filer.", + "settings.autoApprove.tool.glob": "Find filer efter mønster. Tillader filsøgning ved hjælp af glob-mønstre (f.eks. src/**/*.ts).", + "settings.autoApprove.tool.grep": "Søg i filindhold. Tillader regex-baseret søgning i filer.", + "settings.autoApprove.tool.list": "Vis mappeindhold. Tillader visning af filer og mapper i en mappe.", + "settings.autoApprove.tool.bash": "Kør terminalkommandoer. Tillader udførelse af shell-kommandoer (f.eks. git status).", + "settings.autoApprove.tool.task": "Start underagenter. Tillader start af specialiserede underagenter til specifikke opgaver.", + "settings.autoApprove.tool.skill": "Indlæs færdigheder. Tillader indlæsning af foruddefinerede færdigheder efter navn.", + "settings.autoApprove.tool.lsp": "Forespørg sprogserver. Tillader kørsel af sprogserverforespørgsler til kodeintelligens.", "settings.autoApprove.tool.todoreadwrite": "Administrer opgaveliste. Tillader læsning og opdatering af den interne opgaveliste.", - "settings.autoApprove.tool.webfetch": "Hent websider", + "settings.autoApprove.tool.webfetch": "Hent en URL. Tillader hentning af indhold fra en specifik URL.", "settings.autoApprove.tool.websearchcodesearch": "Søg på nettet eller i kode. Tillader udførelse af eksterne web- eller kodesøgninger.", - "settings.autoApprove.tool.external_directory": "Adgang til filer uden for arbejdsområdet", - "settings.autoApprove.tool.doom_loop": "Fortsæt efter gentagne fejl", + "settings.autoApprove.tool.external_directory": "Få adgang til filer uden for arbejdsområdet. Udløses ved adgang til filer uden for den aktuelle projektmappe.", + "settings.autoApprove.tool.doom_loop": "Forhindr gentagne identiske handlinger. Udløses, når det samme værktøjskald gentages med identisk input.", "settings.checkpoints.enable.title": "Aktiver snapshots", "settings.checkpoints.enable.description": "Opret kontrolpunkter før filredigeringer", "settings.context.autoCompaction.title": "Automatisk komprimering", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 7182e52d4bf..c6ca610a325 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -972,22 +972,22 @@ export const dict = { "settings.autoApprove.addPath": "Pfad hinzufügen", "settings.autoApprove.placeholder.command": "z. B. git *", "settings.autoApprove.placeholder.path": "z. B. *.env", - "settings.autoApprove.tool.read": "Dateiinhalte lesen", - "settings.autoApprove.tool.edit": "Dateien bearbeiten oder erstellen", - "settings.autoApprove.tool.glob": "Dateien nach Muster suchen", - "settings.autoApprove.tool.grep": "Dateiinhalte durchsuchen", - "settings.autoApprove.tool.list": "Verzeichnisinhalte auflisten", - "settings.autoApprove.tool.bash": "Shell-Befehle ausführen", - "settings.autoApprove.tool.task": "Unter-Agent-Aufgaben erstellen", - "settings.autoApprove.tool.skill": "Skills ausführen", - "settings.autoApprove.tool.lsp": "Sprachserver-Operationen", + "settings.autoApprove.tool.read": "Dateien lesen. Erlaubt dem Agenten, Dateien zu lesen, die dem angegebenen Pfad entsprechen.", + "settings.autoApprove.tool.edit": "Dateien ändern. Erlaubt dem Agenten, Dateien zu erstellen oder zu bearbeiten, einschließlich Patches und Aktualisierungen mehrerer Dateien.", + "settings.autoApprove.tool.glob": "Dateien nach Muster abgleichen. Erlaubt den Dateiabgleich mithilfe von Glob-Mustern (z. B. src/**/*.ts).", + "settings.autoApprove.tool.grep": "Dateiinhalte durchsuchen. Erlaubt Regex-basierte Suchen innerhalb von Dateien.", + "settings.autoApprove.tool.list": "Verzeichnisinhalte auflisten. Erlaubt das Anzeigen von Dateien und Ordnern innerhalb eines Verzeichnisses.", + "settings.autoApprove.tool.bash": "Terminalbefehle ausführen. Erlaubt die Ausführung von Shell-Befehlen (z. B. git status).", + "settings.autoApprove.tool.task": "Sub-Agenten starten. Erlaubt das Starten spezialisierter Sub-Agenten für bestimmte Aufgaben.", + "settings.autoApprove.tool.skill": "Fähigkeiten laden. Erlaubt das Laden vordefinierter Fähigkeiten nach Namen.", + "settings.autoApprove.tool.lsp": "Sprachserver abfragen. Erlaubt das Ausführen von Sprachserver-Abfragen für Code-Intelligenz.", "settings.autoApprove.tool.todoreadwrite": "Aufgabenliste verwalten. Ermöglicht das Lesen und Aktualisieren der internen Aufgabenliste.", - "settings.autoApprove.tool.webfetch": "Webseiten abrufen", + "settings.autoApprove.tool.webfetch": "URL abrufen. Erlaubt das Abrufen von Inhalten einer bestimmten URL.", "settings.autoApprove.tool.websearchcodesearch": "Web oder Code durchsuchen. Ermöglicht die Durchführung externer Web- oder Code-Suchen.", - "settings.autoApprove.tool.external_directory": "Auf Dateien außerhalb des Arbeitsbereichs zugreifen", - "settings.autoApprove.tool.doom_loop": "Nach wiederholten Fehlern fortfahren", + "settings.autoApprove.tool.external_directory": "Zugriff auf Dateien außerhalb des Arbeitsbereichs. Wird ausgelöst, wenn auf Dateien außerhalb des aktuellen Projektverzeichnisses zugegriffen wird.", + "settings.autoApprove.tool.doom_loop": "Wiederholte identische Aktionen verhindern. Wird ausgelöst, wenn sich derselbe Werkzeugaufruf mit identischer Eingabe wiederholt.", "settings.checkpoints.enable.title": "Snapshots aktivieren", "settings.checkpoints.enable.description": "Prüfpunkte vor Dateibearbeitungen erstellen, um vorherige Zustände wiederherstellen zu können", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 6c9641be6db..a3a570f7f10 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -964,22 +964,22 @@ export const dict = { "settings.autoApprove.addPath": "Agregar ruta", "settings.autoApprove.placeholder.command": "ej. git *", "settings.autoApprove.placeholder.path": "ej. *.env", - "settings.autoApprove.tool.read": "Leer contenido de archivos", - "settings.autoApprove.tool.edit": "Editar o crear archivos", - "settings.autoApprove.tool.glob": "Buscar archivos por patrón", - "settings.autoApprove.tool.grep": "Buscar contenido de archivos", - "settings.autoApprove.tool.list": "Listar contenido de directorio", - "settings.autoApprove.tool.bash": "Ejecutar comandos de shell", - "settings.autoApprove.tool.task": "Crear tareas de sub-agente", - "settings.autoApprove.tool.skill": "Ejecutar habilidades", - "settings.autoApprove.tool.lsp": "Operaciones del servidor de lenguaje", + "settings.autoApprove.tool.read": "Leer archivos. Permite que el agente lea archivos que coincidan con la ruta especificada.", + "settings.autoApprove.tool.edit": "Modificar archivos. Permite que el agente cree o edite archivos, incluyendo parches y actualizaciones de múltiples archivos.", + "settings.autoApprove.tool.glob": "Buscar archivos por patrón. Permite la búsqueda de archivos mediante patrones glob (ej. src/**/*.ts).", + "settings.autoApprove.tool.grep": "Buscar en el contenido de archivos. Permite la búsqueda basada en expresiones regulares dentro de los archivos.", + "settings.autoApprove.tool.list": "Listar contenido del directorio. Permite ver archivos y carpetas dentro de un directorio.", + "settings.autoApprove.tool.bash": "Ejecutar comandos de terminal. Permite la ejecución de comandos de shell (ej. git status).", + "settings.autoApprove.tool.task": "Iniciar subagentes. Permite iniciar subagentes especializados para tareas específicas.", + "settings.autoApprove.tool.skill": "Cargar habilidades. Permite cargar habilidades predefinidas por su nombre.", + "settings.autoApprove.tool.lsp": "Consultar servidor de lenguaje. Permite ejecutar consultas al servidor de lenguaje para la inteligencia del código.", "settings.autoApprove.tool.todoreadwrite": "Administrar lista de tareas. Permite leer y actualizar la lista de tareas interna.", - "settings.autoApprove.tool.webfetch": "Obtener páginas web", + "settings.autoApprove.tool.webfetch": "Obtener una URL. Permite recuperar contenido de una URL específica.", "settings.autoApprove.tool.websearchcodesearch": "Buscar en la web o en el código. Permite realizar búsquedas externas en la web o en el código.", - "settings.autoApprove.tool.external_directory": "Acceder a archivos fuera del espacio de trabajo", - "settings.autoApprove.tool.doom_loop": "Continuar tras fallos repetidos", + "settings.autoApprove.tool.external_directory": "Acceder a archivos fuera del espacio de trabajo. Se activa al acceder a archivos fuera del directorio del proyecto actual.", + "settings.autoApprove.tool.doom_loop": "Evitar acciones idénticas repetidas. Se activa cuando se repite la misma llamada de herramienta con entrada idéntica.", "settings.checkpoints.enable.title": "Habilitar instantáneas", "settings.checkpoints.enable.description": "Crear puntos de control antes de editar archivos", "settings.context.autoCompaction.title": "Compactación automática", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 7fd1da866a0..d717cae767c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -973,22 +973,22 @@ export const dict = { "settings.autoApprove.addPath": "Ajouter un chemin", "settings.autoApprove.placeholder.command": "ex. git *", "settings.autoApprove.placeholder.path": "ex. *.env", - "settings.autoApprove.tool.read": "Lire le contenu des fichiers", - "settings.autoApprove.tool.edit": "Modifier ou créer des fichiers", - "settings.autoApprove.tool.glob": "Rechercher des fichiers par motif", - "settings.autoApprove.tool.grep": "Rechercher le contenu des fichiers", - "settings.autoApprove.tool.list": "Lister le contenu du répertoire", - "settings.autoApprove.tool.bash": "Exécuter des commandes shell", - "settings.autoApprove.tool.task": "Créer des tâches de sous-agent", - "settings.autoApprove.tool.skill": "Exécuter des compétences", - "settings.autoApprove.tool.lsp": "Opérations du serveur de langage", + "settings.autoApprove.tool.read": "Lire des fichiers. Permet à l'agent de lire les fichiers correspondant au chemin spécifié.", + "settings.autoApprove.tool.edit": "Modifier des fichiers. Permet à l'agent de créer ou d'éditer des fichiers, y compris les patchs et les mises à jour de plusieurs fichiers.", + "settings.autoApprove.tool.glob": "Faire correspondre les fichiers par modèle. Permet la correspondance de fichiers à l'aide de modèles glob (ex. src/**/*.ts).", + "settings.autoApprove.tool.grep": "Rechercher dans le contenu des fichiers. Permet la recherche basée sur les expressions régulières dans les fichiers.", + "settings.autoApprove.tool.list": "Lister le contenu du répertoire. Permet de visualiser les fichiers et dossiers dans un répertoire.", + "settings.autoApprove.tool.bash": "Exécuter des commandes de terminal. Permet l'exécution de commandes shell (ex. git status).", + "settings.autoApprove.tool.task": "Lancer des sous-agents. Permet de démarrer des sous-agents spécialisés pour des tâches spécifiques.", + "settings.autoApprove.tool.skill": "Charger des compétences. Permet de charger des compétences prédéfinies par leur nom.", + "settings.autoApprove.tool.lsp": "Interroger le serveur de langage. Permet d'exécuter des requêtes de serveur de langage pour l'intelligence du code.", "settings.autoApprove.tool.todoreadwrite": "Gérer la liste des tâches. Permet de lire et de mettre à jour la liste des tâches interne.", - "settings.autoApprove.tool.webfetch": "Récupérer des pages web", + "settings.autoApprove.tool.webfetch": "Récupérer une URL. Permet de récupérer le contenu d'une URL spécifique.", "settings.autoApprove.tool.websearchcodesearch": "Rechercher sur le Web ou dans le code. Permet d'effectuer des recherches externes sur le Web ou dans le code.", - "settings.autoApprove.tool.external_directory": "Accéder aux fichiers hors de l'espace de travail", - "settings.autoApprove.tool.doom_loop": "Continuer après des échecs répétés", + "settings.autoApprove.tool.external_directory": "Accéder aux fichiers en dehors de l'espace de travail. Déclenché lors de l'accès à des fichiers en dehors du répertoire de projet actuel.", + "settings.autoApprove.tool.doom_loop": "Prévenir les actions identiques répétées. Déclenché lorsque le même appel d'outil se répète avec une entrée identique.", "settings.checkpoints.enable.title": "Activer les instantanés", "settings.checkpoints.enable.description": "Créer des points de contrôle avant les modifications de fichiers", "settings.context.autoCompaction.title": "Compaction automatique", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index c002f837880..0ace16ca346 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -956,21 +956,21 @@ export const dict = { "settings.autoApprove.addPath": "パスを追加", "settings.autoApprove.placeholder.command": "例: git *", "settings.autoApprove.placeholder.path": "例: *.env", - "settings.autoApprove.tool.read": "ファイルの内容を読み取る", - "settings.autoApprove.tool.edit": "ファイルを編集または作成", - "settings.autoApprove.tool.glob": "パターンでファイルを検索", - "settings.autoApprove.tool.grep": "ファイルの内容を検索", - "settings.autoApprove.tool.list": "ディレクトリの内容を一覧表示", - "settings.autoApprove.tool.bash": "シェルコマンドを実行", - "settings.autoApprove.tool.task": "サブエージェントタスクを作成", - "settings.autoApprove.tool.skill": "スキルを実行", - "settings.autoApprove.tool.lsp": "言語サーバー操作", + "settings.autoApprove.tool.read": "ファイルを読み取る。指定されたパスに一致するファイルをエージェントが読み取ることを許可します。", + "settings.autoApprove.tool.edit": "ファイルを変更する。パッチや複数ファイルの更新を含め、エージェントがファイルを作成または編集することを許可します。", + "settings.autoApprove.tool.glob": "パターンでファイルを一致。globパターン(例:src/**/*.ts)を使用したファイルの一致を許可します。", + "settings.autoApprove.tool.grep": "ファイル内容を検索。ファイル内の正規表現ベースの検索を許可します。", + "settings.autoApprove.tool.list": "ディレクトリの内容を一覧表示。ディレクトリ内のファイルとフォルダの表示を許可します。", + "settings.autoApprove.tool.bash": "ターミナルコマンドを実行。シェルコマンド(例:git status)の実行を許可します。", + "settings.autoApprove.tool.task": "サブエージェントを起動。特定のタスク用に特化したサブエージェントを開始することを許可します。", + "settings.autoApprove.tool.skill": "スキルを読み込む。事前定義されたスキルを名前で読み込むことを許可します。", + "settings.autoApprove.tool.lsp": "言語サーバーにクエリ。コードインテリジェンスのために言語サーバークエリの実行を許可します。", "settings.autoApprove.tool.todoreadwrite": "タスクリストの管理。内部タスクリストの読み取りと更新を許可します。", - "settings.autoApprove.tool.webfetch": "ウェブページを取得", + "settings.autoApprove.tool.webfetch": "URLをフェッチ。特定のURLからコンテンツを取得することを許可します。", "settings.autoApprove.tool.websearchcodesearch": "Webまたはコードの検索。外部のWebまたはコード検索の実行を許可します。", - "settings.autoApprove.tool.external_directory": "ワークスペース外のファイルにアクセス", - "settings.autoApprove.tool.doom_loop": "繰り返しの失敗後に続行", + "settings.autoApprove.tool.external_directory": "ワークスペース外のファイルにアクセス。現在のプロジェクトディレクトリ外のファイルにアクセスしたときにトリガーされます。", + "settings.autoApprove.tool.doom_loop": "繰り返し同一のアクションを防止。同じツール呼び出しが同一の入力で繰り返されたときにトリガーされます。", "settings.checkpoints.enable.title": "スナップショットを有効にする", "settings.checkpoints.enable.description": "ファイル編集前にチェックポイントを作成して以前の状態を復元可能にする", "settings.context.autoCompaction.title": "自動圧縮", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 4d0355fc87c..a726637f107 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -953,21 +953,21 @@ export const dict = { "settings.autoApprove.addPath": "경로 추가", "settings.autoApprove.placeholder.command": "예: git *", "settings.autoApprove.placeholder.path": "예: *.env", - "settings.autoApprove.tool.read": "파일 내용 읽기", - "settings.autoApprove.tool.edit": "파일 편집 또는 생성", - "settings.autoApprove.tool.glob": "패턴으로 파일 찾기", - "settings.autoApprove.tool.grep": "파일 내용 검색", - "settings.autoApprove.tool.list": "디렉토리 내용 나열", - "settings.autoApprove.tool.bash": "셸 명령 실행", - "settings.autoApprove.tool.task": "하위 에이전트 작업 생성", - "settings.autoApprove.tool.skill": "스킬 실행", - "settings.autoApprove.tool.lsp": "언어 서버 작업", + "settings.autoApprove.tool.read": "파일 읽기. 에이전트가 지정된 경로와 일치하는 파일을 읽을 수 있도록 허용합니다.", + "settings.autoApprove.tool.edit": "파일 수정. 패치 및 다중 파일 업데이트를 포함하여 에이전트가 파일을 생성하거나 편집할 수 있도록 허용합니다.", + "settings.autoApprove.tool.glob": "패턴으로 파일 찾기. glob 패턴(예: src/**/*.ts)을 사용한 파일 찾기를 허용합니다.", + "settings.autoApprove.tool.grep": "파일 내용 검색. 파일 내에서 정규식 기반 검색을 허용합니다.", + "settings.autoApprove.tool.list": "디렉토리 내용 나열. 디렉토리 내의 파일 및 폴더 보기를 허용합니다.", + "settings.autoApprove.tool.bash": "터미널 명령어 실행. 셸 명령어(예: git status) 실행을 허용합니다.", + "settings.autoApprove.tool.task": "하위 에이전트 시작. 특정 작업을 위한 특수 하위 에이전트 시작을 허용합니다.", + "settings.autoApprove.tool.skill": "기술 로드. 사전 정의된 기술을 이름으로 로드할 수 있도록 허용합니다.", + "settings.autoApprove.tool.lsp": "언어 서버 쿼리. 코드 인텔리전스를 위해 언어 서버 쿼리 실행을 허용합니다.", "settings.autoApprove.tool.todoreadwrite": "작업 목록 관리. 내부 작업 목록을 읽고 업데이트할 수 있도록 허용합니다.", - "settings.autoApprove.tool.webfetch": "웹 페이지 가져오기", + "settings.autoApprove.tool.webfetch": "URL 가져오기. 특정 URL에서 콘텐츠를 검색할 수 있도록 허용합니다.", "settings.autoApprove.tool.websearchcodesearch": "웹 또는 코드 검색. 외부 웹 또는 코드 검색을 수행할 수 있도록 허용합니다.", - "settings.autoApprove.tool.external_directory": "워크스페이스 외부 파일 접근", - "settings.autoApprove.tool.doom_loop": "반복 실패 후 계속", + "settings.autoApprove.tool.external_directory": "작업 공간 외부 파일 접근. 현재 프로젝트 디렉토리 외부의 파일에 접근할 때 트리거됩니다.", + "settings.autoApprove.tool.doom_loop": "반복되는 동일한 작업 방지. 동일한 입력으로 동일한 도구 호출이 반복될 때 트리거됩니다.", "settings.checkpoints.enable.title": "스냅샷 활성화", "settings.checkpoints.enable.description": "파일 편집 전 체크포인트를 생성하여 이전 상태를 복원할 수 있습니다", "settings.context.autoCompaction.title": "자동 압축", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 15cd83c508d..c29631ea03e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -958,22 +958,22 @@ export const dict = { "settings.autoApprove.addPath": "Legg til sti", "settings.autoApprove.placeholder.command": "f.eks. git *", "settings.autoApprove.placeholder.path": "f.eks. *.env", - "settings.autoApprove.tool.read": "Les filinnhold", - "settings.autoApprove.tool.edit": "Rediger eller opprett filer", - "settings.autoApprove.tool.glob": "Finn filer etter mønster", - "settings.autoApprove.tool.grep": "Søk i filinnhold", - "settings.autoApprove.tool.list": "List mappeinnhold", - "settings.autoApprove.tool.bash": "Kjør skallkommandoer", - "settings.autoApprove.tool.task": "Opprett underagentoppgaver", - "settings.autoApprove.tool.skill": "Kjør ferdigheter", - "settings.autoApprove.tool.lsp": "Språkserveroperasjoner", + "settings.autoApprove.tool.read": "Les filer. Tillater agenten å lese filer som samsvarer med den angitte stien.", + "settings.autoApprove.tool.edit": "Endre filer. Tillater agenten å opprette eller redigere filer, inkludert oppdateringer og flerfilsoppdateringer.", + "settings.autoApprove.tool.glob": "Søk etter filer med mønster. Tillater filsøk ved bruk av glob-mønstre (f.eks. src/**/*.ts).", + "settings.autoApprove.tool.grep": "Søk i filinnhold. Tillater regex-basert søk inne i filer.", + "settings.autoApprove.tool.list": "Vis kataloginnhold. Tillater visning av filer og mapper i en katalog.", + "settings.autoApprove.tool.bash": "Kjør terminalkommandoer. Tillater kjøring av shell-kommandoer (f.eks. git status).", + "settings.autoApprove.tool.task": "Start underagenter. Tillater start av spesialiserte underagenter for spesifikke oppgaver.", + "settings.autoApprove.tool.skill": "Last inn ferdigheter. Tillater innlasting av forhåndsdefinerte ferdigheter etter navn.", + "settings.autoApprove.tool.lsp": "Spør språkserver. Tillater kjøring av språkserverforespørsler for kodeintelligens.", "settings.autoApprove.tool.todoreadwrite": "Administrer oppgaveliste. Tillater lesing og oppdatering av den interne oppgavelisten.", - "settings.autoApprove.tool.webfetch": "Hent nettsider", + "settings.autoApprove.tool.webfetch": "Hent en URL. Tillater henting av innhold fra en spesifikk URL.", "settings.autoApprove.tool.websearchcodesearch": "Søk på nettet eller i kode. Tillater å utføre eksterne nett- eller kodesøk.", - "settings.autoApprove.tool.external_directory": "Tilgang til filer utenfor arbeidsområdet", - "settings.autoApprove.tool.doom_loop": "Fortsett etter gjentatte feil", + "settings.autoApprove.tool.external_directory": "Få tilgang til filer utenfor arbeidsområdet. Utløses ved tilgang til filer utenfor gjeldende prosjektkatalog.", + "settings.autoApprove.tool.doom_loop": "Forhindre gjentatte identiske handlinger. Utløses når det samme verktøyanropet gjentas med identisk inndata.", "settings.checkpoints.enable.title": "Aktiver øyeblikksbilder", "settings.checkpoints.enable.description": "Opprett kontrollpunkter før filredigeringer", "settings.context.autoCompaction.title": "Automatisk komprimering", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 34739662b13..38d87c8e6ee 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -959,22 +959,22 @@ export const dict = { "settings.autoApprove.addPath": "Dodaj ścieżkę", "settings.autoApprove.placeholder.command": "np. git *", "settings.autoApprove.placeholder.path": "np. *.env", - "settings.autoApprove.tool.read": "Odczyt zawartości plików", - "settings.autoApprove.tool.edit": "Edycja lub tworzenie plików", - "settings.autoApprove.tool.glob": "Wyszukiwanie plików według wzorca", - "settings.autoApprove.tool.grep": "Przeszukiwanie zawartości plików", - "settings.autoApprove.tool.list": "Listowanie zawartości katalogu", - "settings.autoApprove.tool.bash": "Wykonywanie poleceń powłoki", - "settings.autoApprove.tool.task": "Tworzenie podzadań agenta", - "settings.autoApprove.tool.skill": "Wykonywanie umiejętności", - "settings.autoApprove.tool.lsp": "Operacje serwera języka", + "settings.autoApprove.tool.read": "Odczytywanie plików. Pozwala agentowi na odczytywanie plików pasujących do określonej ścieżki.", + "settings.autoApprove.tool.edit": "Modyfikowanie plików. Pozwala agentowi na tworzenie lub edytowanie plików, w tym wprowadzanie poprawek i aktualizację wielu plików.", + "settings.autoApprove.tool.glob": "Dopasowywanie plików do wzorca. Pozwala na dopasowywanie plików za pomocą wzorców glob (np. src/**/*.ts).", + "settings.autoApprove.tool.grep": "Przeszukiwanie zawartości plików. Pozwala na wyszukiwanie oparte na wyrażeniach regularnych wewnątrz plików.", + "settings.autoApprove.tool.list": "Wyświetlanie zawartości katalogu. Pozwala na przeglądanie plików i folderów w katalogu.", + "settings.autoApprove.tool.bash": "Uruchamianie poleceń terminala. Pozwala na wykonanie poleceń powłoki (np. git status).", + "settings.autoApprove.tool.task": "Uruchamianie podagentów. Pozwala na uruchamianie wyspecjalizowanych podagentów do określonych zadań.", + "settings.autoApprove.tool.skill": "Ładowanie umiejętności. Pozwala na ładowanie predefiniowanych umiejętności po nazwie.", + "settings.autoApprove.tool.lsp": "Zapytania do serwera języka. Pozwala na uruchamianie zapytań do serwera języka w celu analizy kodu.", "settings.autoApprove.tool.todoreadwrite": "Zarządzaj listą zadań. Pozwala na odczytywanie i aktualizowanie wewnętrznej listy zadań.", - "settings.autoApprove.tool.webfetch": "Pobieranie stron internetowych", + "settings.autoApprove.tool.webfetch": "Pobieranie URL. Pozwala na pobieranie zawartości z określonego adresu URL.", "settings.autoApprove.tool.websearchcodesearch": "Przeszukuj sieć lub kod. Pozwala na wykonywanie zewnętrznych wyszukiwań w sieci lub kodzie.", - "settings.autoApprove.tool.external_directory": "Dostęp do plików poza obszarem roboczym", - "settings.autoApprove.tool.doom_loop": "Kontynuuj po powtarzających się błędach", + "settings.autoApprove.tool.external_directory": "Dostęp do plików poza obszarem roboczym. Uruchamiane przy dostępie do plików poza bieżącym katalogiem projektu.", + "settings.autoApprove.tool.doom_loop": "Zapobieganie powtarzaniu tych samych akcji. Uruchamiane, gdy to samo wywołanie narzędzia powtarza się z identycznymi danymi wejściowymi.", "settings.checkpoints.enable.title": "Włącz migawki", "settings.checkpoints.enable.description": "Twórz punkty kontrolne przed edycją plików", "settings.context.autoCompaction.title": "Automatyczna kompakcja", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index a4791e6d7d6..421cd9f2cb8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -961,22 +961,22 @@ export const dict = { "settings.autoApprove.addPath": "Добавить путь", "settings.autoApprove.placeholder.command": "например, git *", "settings.autoApprove.placeholder.path": "например, *.env", - "settings.autoApprove.tool.read": "Чтение содержимого файлов", - "settings.autoApprove.tool.edit": "Редактирование или создание файлов", - "settings.autoApprove.tool.glob": "Поиск файлов по шаблону", - "settings.autoApprove.tool.grep": "Поиск в содержимом файлов", - "settings.autoApprove.tool.list": "Список содержимого каталога", - "settings.autoApprove.tool.bash": "Выполнение команд оболочки", - "settings.autoApprove.tool.task": "Создание подзадач агента", - "settings.autoApprove.tool.skill": "Выполнение навыков", - "settings.autoApprove.tool.lsp": "Операции языкового сервера", + "settings.autoApprove.tool.read": "Чтение файлов. Разрешает агенту читать файлы, соответствующие указанному пути.", + "settings.autoApprove.tool.edit": "Изменение файлов. Разрешает агенту создавать или редактировать файлы, включая патчи и обновления нескольких файлов.", + "settings.autoApprove.tool.glob": "Поиск файлов по шаблону. Разрешает поиск файлов с использованием glob-шаблонов (например, src/**/*.ts).", + "settings.autoApprove.tool.grep": "Поиск по содержимому файлов. Разрешает поиск на основе регулярных выражений внутри файлов.", + "settings.autoApprove.tool.list": "Просмотр содержимого каталогов. Разрешает просмотр файлов и папок внутри каталога.", + "settings.autoApprove.tool.bash": "Выполнение команд терминала. Разрешает выполнение команд оболочки (например, git status).", + "settings.autoApprove.tool.task": "Запуск субагентов. Разрешает запуск специализированных субагентов для конкретных задач.", + "settings.autoApprove.tool.skill": "Загрузка навыков. Разрешает загрузку предопределенных навыков по имени.", + "settings.autoApprove.tool.lsp": "Запрос к языковому серверу. Разрешает выполнение запросов к языковому серверу для анализа кода.", "settings.autoApprove.tool.todoreadwrite": "Управление списком задач. Разрешает чтение и обновление внутреннего списка задач.", - "settings.autoApprove.tool.webfetch": "Получение веб-страниц", + "settings.autoApprove.tool.webfetch": "Получение URL. Разрешает извлечение содержимого по указанному URL-адресу.", "settings.autoApprove.tool.websearchcodesearch": "Поиск в интернете или коде. Разрешает выполнение внешних поисковых запросов в интернете или коде.", - "settings.autoApprove.tool.external_directory": "Доступ к файлам вне рабочей области", - "settings.autoApprove.tool.doom_loop": "Продолжить после повторных сбоев", + "settings.autoApprove.tool.external_directory": "Доступ к файлам вне рабочей области. Срабатывает при доступе к файлам за пределами текущего каталога проекта.", + "settings.autoApprove.tool.doom_loop": "Предотвращение повторных идентичных действий. Срабатывает, когда один и тот же вызов инструмента повторяется с идентичными входными данными.", "settings.checkpoints.enable.title": "Включить снимки", "settings.checkpoints.enable.description": "Создавать контрольные точки перед редактированием файлов", "settings.context.autoCompaction.title": "Автоматическое сжатие", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index ca6c6c722a1..0259b46ad1f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -949,20 +949,20 @@ export const dict = { "settings.autoApprove.addPath": "เพิ่มพาธ", "settings.autoApprove.placeholder.command": "เช่น git *", "settings.autoApprove.placeholder.path": "เช่น *.env", - "settings.autoApprove.tool.read": "อ่านเนื้อหาไฟล์", - "settings.autoApprove.tool.edit": "แก้ไขหรือสร้างไฟล์", - "settings.autoApprove.tool.glob": "ค้นหาไฟล์ตามรูปแบบ", - "settings.autoApprove.tool.grep": "ค้นหาเนื้อหาไฟล์", - "settings.autoApprove.tool.list": "แสดงเนื้อหาไดเรกทอรี", - "settings.autoApprove.tool.bash": "เรียกใช้คำสั่ง shell", - "settings.autoApprove.tool.task": "สร้างงานเอเจนต์ย่อย", - "settings.autoApprove.tool.skill": "เรียกใช้ทักษะ", - "settings.autoApprove.tool.lsp": "การดำเนินการเซิร์ฟเวอร์ภาษา", + "settings.autoApprove.tool.read": "อ่านไฟล์ อนุญาตให้ตัวแทน (Agent) อ่านไฟล์ที่ตรงกับเส้นทางที่ระบุ", + "settings.autoApprove.tool.edit": "แก้ไขไฟล์ อนุญาตให้ตัวแทน (Agent) สร้างหรือแก้ไขไฟล์ รวมถึงแพตช์ (Patch) และการอัปเดตหลายไฟล์", + "settings.autoApprove.tool.glob": "จับคู่ไฟล์ด้วยรูปแบบ อนุญาตให้จับคู่ไฟล์โดยใช้รูปแบบ glob (เช่น src/**/*.ts)", + "settings.autoApprove.tool.grep": "ค้นหาเนื้อหาไฟล์ อนุญาตให้ค้นหาเนื้อหาภายในไฟล์โดยใช้นิพจน์ทั่วไป (Regex)", + "settings.autoApprove.tool.list": "แสดงเนื้อหาในไดเรกทอรี อนุญาตให้ดูไฟล์และโฟลเดอร์ภายในไดเรกทอรี", + "settings.autoApprove.tool.bash": "เรียกใช้คำสั่งเทอร์มินัล อนุญาตให้ดำเนินการคำสั่งเชลล์ (เช่น git status)", + "settings.autoApprove.tool.task": "เปิดใช้งานตัวแทนย่อย อนุญาตให้เริ่มต้นตัวแทนย่อยเฉพาะทางสำหรับงานเฉพาะ", + "settings.autoApprove.tool.skill": "โหลดทักษะ อนุญาตให้โหลดทักษะที่กำหนดไว้ล่วงหน้าตามชื่อ", + "settings.autoApprove.tool.lsp": "สอบถามเซิร์ฟเวอร์ภาษา อนุญาตให้เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษาสำหรับระบบอัจฉริยะของโค้ด", "settings.autoApprove.tool.todoreadwrite": "จัดการรายการงาน อนุญาตให้อ่านและอัปเดตรายการงานภายใน", - "settings.autoApprove.tool.webfetch": "ดึงหน้าเว็บ", + "settings.autoApprove.tool.webfetch": "ดึงข้อมูลจาก URL อนุญาตให้ดึงเนื้อหาจาก URL ที่ระบุ", "settings.autoApprove.tool.websearchcodesearch": "ค้นหาเว็บหรือโค้ด อนุญาตให้ทำการค้นหาเว็บหรือโค้ดภายนอก", - "settings.autoApprove.tool.external_directory": "เข้าถึงไฟล์นอกพื้นที่ทำงาน", - "settings.autoApprove.tool.doom_loop": "ดำเนินต่อหลังจากล้มเหลวซ้ำ", + "settings.autoApprove.tool.external_directory": "เข้าถึงไฟล์นอกพื้นที่ทำงาน (Workspace) ถูกเรียกเมื่อเข้าถึงไฟล์ภายนอกไดเรกทอรีโปรเจ็กต์ปัจจุบัน", + "settings.autoApprove.tool.doom_loop": "ป้องกันการกระทำที่ซ้ำกัน ถูกเรียกเมื่อเครื่องมือเดิมถูกเรียกซ้ำด้วยข้อมูลนำเข้าที่เหมือนกัน", "settings.checkpoints.enable.title": "เปิดใช้งานสแนปชอต", "settings.checkpoints.enable.description": "สร้างจุดตรวจก่อนแก้ไขไฟล์", "settings.context.autoCompaction.title": "การบีบอัดอัตโนมัติ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 809df8773b1..7dfe292be5e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -942,20 +942,20 @@ export const dict = { "settings.autoApprove.addPath": "添加路径", "settings.autoApprove.placeholder.command": "例如:git *", "settings.autoApprove.placeholder.path": "例如:*.env", - "settings.autoApprove.tool.read": "读取文件内容", - "settings.autoApprove.tool.edit": "编辑或创建文件", - "settings.autoApprove.tool.glob": "按模式查找文件", - "settings.autoApprove.tool.grep": "搜索文件内容", - "settings.autoApprove.tool.list": "列出目录内容", - "settings.autoApprove.tool.bash": "执行 shell 命令", - "settings.autoApprove.tool.task": "创建子智能体任务", - "settings.autoApprove.tool.skill": "执行技能", - "settings.autoApprove.tool.lsp": "语言服务器操作", + "settings.autoApprove.tool.read": "读取文件。允许代理读取符合指定路径的文件。", + "settings.autoApprove.tool.edit": "修改文件。允许代理创建或编辑文件,包括补丁和多文件更新。", + "settings.autoApprove.tool.glob": "按模式匹配文件。允许使用 glob 模式匹配文件(例如 src/**/*.ts)。", + "settings.autoApprove.tool.grep": "搜索文件内容。允许在文件内进行基于正则表达式的搜索。", + "settings.autoApprove.tool.list": "列出目录内容。允许查看目录内的文件和文件夹。", + "settings.autoApprove.tool.bash": "运行终端命令。允许执行 shell 命令(例如 git status)。", + "settings.autoApprove.tool.task": "启动子代理。允许启动执行特定任务的专用子代理。", + "settings.autoApprove.tool.skill": "加载技能。允许按名称加载预定义的技能。", + "settings.autoApprove.tool.lsp": "查询语言服务器。允许运行语言服务器查询以获取代码智能。", "settings.autoApprove.tool.todoreadwrite": "管理任务列表。允许读取和更新内部任务列表。", - "settings.autoApprove.tool.webfetch": "获取网页", + "settings.autoApprove.tool.webfetch": "获取 URL。允许从特定 URL 检索内容。", "settings.autoApprove.tool.websearchcodesearch": "搜索 Web 或代码。允许执行外部 Web 或代码搜索。", - "settings.autoApprove.tool.external_directory": "访问工作区外的文件", - "settings.autoApprove.tool.doom_loop": "重复失败后继续", + "settings.autoApprove.tool.external_directory": "访问工作区外部的文件。访问当前项目目录之外的文件时触发。", + "settings.autoApprove.tool.doom_loop": "防止重复相同操作。当相同的工具调用以相同的输入重复时触发。", "settings.checkpoints.enable.title": "启用快照", "settings.checkpoints.enable.description": "在文件编辑前创建检查点,以便恢复之前的状态", "settings.context.autoCompaction.title": "自动压缩", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index fba151d38e2..3f58b2fee20 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -943,20 +943,20 @@ export const dict = { "settings.autoApprove.addPath": "新增路徑", "settings.autoApprove.placeholder.command": "例如:git *", "settings.autoApprove.placeholder.path": "例如:*.env", - "settings.autoApprove.tool.read": "讀取檔案內容", - "settings.autoApprove.tool.edit": "編輯或建立檔案", - "settings.autoApprove.tool.glob": "按模式尋找檔案", - "settings.autoApprove.tool.grep": "搜尋檔案內容", - "settings.autoApprove.tool.list": "列出目錄內容", - "settings.autoApprove.tool.bash": "執行 shell 命令", - "settings.autoApprove.tool.task": "建立 sub-agent 任務", - "settings.autoApprove.tool.skill": "執行技能", - "settings.autoApprove.tool.lsp": "語言伺服器操作", + "settings.autoApprove.tool.read": "讀取檔案。允許代理讀取符合指定路徑的檔案。", + "settings.autoApprove.tool.edit": "修改檔案。允許代理建立或編輯檔案,包括修補程式和多檔案更新。", + "settings.autoApprove.tool.glob": "依模式匹配檔案。允許使用 glob 模式匹配檔案(例如 src/**/*.ts)。", + "settings.autoApprove.tool.grep": "搜尋檔案內容。允許在檔案內進行基於正規表示式的搜尋。", + "settings.autoApprove.tool.list": "列出目錄內容。允許檢視目錄內的檔案和資料夾。", + "settings.autoApprove.tool.bash": "執行終端機命令。允許執行 shell 命令(例如 git status)。", + "settings.autoApprove.tool.task": "啟動子代理。允許啟動執行特定任務的專用子代理。", + "settings.autoApprove.tool.skill": "載入技能。允許依名稱載入預先定義的技能。", + "settings.autoApprove.tool.lsp": "查詢語言伺服器。允許執行語言伺服器查詢以取得程式碼智慧。", "settings.autoApprove.tool.todoreadwrite": "管理任務清單。允許讀取和更新內部任務清單。", - "settings.autoApprove.tool.webfetch": "擷取網頁", + "settings.autoApprove.tool.webfetch": "擷取 URL。允許從特定 URL 擷取內容。", "settings.autoApprove.tool.websearchcodesearch": "搜尋網路或程式碼。允許執行外部網路或程式碼搜尋。", - "settings.autoApprove.tool.external_directory": "存取工作區外的檔案", - "settings.autoApprove.tool.doom_loop": "重複失敗後繼續", + "settings.autoApprove.tool.external_directory": "存取工作區外部的檔案。存取目前專案目錄之外的檔案時觸發。", + "settings.autoApprove.tool.doom_loop": "防止重複相同操作。當相同的工具呼叫以相同的輸入重複時觸發。", "settings.checkpoints.enable.title": "啟用快照", "settings.checkpoints.enable.description": "在檔案編輯前建立檢查點,以便恢復之前的狀態", "settings.context.autoCompaction.title": "自動壓縮", From 0a1da7a8720f68db568d5cf20b6a9f05c1629898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 10 Mar 2026 00:31:00 -0300 Subject: [PATCH 16/17] fix: update flake to get bun 1.3.10 --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 9efa1883b18..80484256d69 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1770812194, - "narHash": "sha256-OH+lkaIKAvPXR3nITO7iYZwew2nW9Y7Xxq0yfM/UcUU=", + "lastModified": 1772956932, + "narHash": "sha256-M0yS4AafhKxPPmOHGqIV0iKxgNO8bHDWdl1kOwGBwRY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8482c7ded03bae7550f3d69884f1e611e3bd19e8", + "rev": "608d0cadfed240589a7eea422407a547ad626a14", "type": "github" }, "original": { From d5de298cb8114c55d9eb4b78605175818f5308f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 10 Mar 2026 00:32:50 -0300 Subject: [PATCH 17/17] fix: permission config string to object update --- packages/opencode/src/config/config.ts | 30 +++++++- .../test/kilocode/patch-jsonc.test.ts | 74 +++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/test/kilocode/patch-jsonc.test.ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 460ecf259d1..158bb850f3e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -12,13 +12,17 @@ import { lazy } from "../util/lazy" import { NamedError } from "@opencode-ai/util/error" import { Flag } from "../flag/flag" import { Auth } from "../auth" +// kilocode_change start import { type ParseError as JsoncParseError, applyEdits, + findNodeAtLocation, modify, parse as parseJsonc, + parseTree, printParseErrorCode, } from "jsonc-parser" +// kilocode_change end import { Instance } from "../project/instance" import { LSPServer } from "../lsp/server" import { BunProc } from "@/bun" @@ -1130,10 +1134,12 @@ export namespace Config { .optional() .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), // kilocode_change start - nullable for delete sentinel - model: ModelId.nullable().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), - small_model: ModelId.nullable().describe( - "Small model to use for tasks like title generation in the format of provider/model", - ).optional(), + model: ModelId.nullable() + .describe("Model to use in the format of provider/model, eg anthropic/claude-2") + .optional(), + small_model: ModelId.nullable() + .describe("Small model to use for tasks like title generation in the format of provider/model") + .optional(), // kilocode_change end // kilocode_change start - renamed from "build" to "code" default_agent: z @@ -1457,6 +1463,22 @@ export namespace Config { return applyEdits(input, edits) } + // kilocode_change start — when the existing JSONC node at this path is a + // scalar (e.g. permission.bash is "ask" as a string), jsonc-parser cannot + // add child keys to it. Detect this case and replace the whole node with + // the patch object in a single modify() call instead of recursing. + if (path.length > 0) { + const tree = parseTree(input) + const node = tree && findNodeAtLocation(tree, path) + if (node && node.type !== "object") { + const edits = modify(input, path, patch, { + formattingOptions: { insertSpaces: true, tabSize: 2 }, + }) + return applyEdits(input, edits) + } + } + // kilocode_change end + return Object.entries(patch).reduce((result, [key, value]) => { if (value === undefined) return result return patchJsonc(result, value, [...path, key]) diff --git a/packages/opencode/test/kilocode/patch-jsonc.test.ts b/packages/opencode/test/kilocode/patch-jsonc.test.ts new file mode 100644 index 00000000000..f511f8f9ce0 --- /dev/null +++ b/packages/opencode/test/kilocode/patch-jsonc.test.ts @@ -0,0 +1,74 @@ +// kilocode_change - new file +import { describe, expect, test } from "bun:test" +import { applyEdits, modify, findNodeAtLocation, parseTree } from "jsonc-parser" + +// Replicate patchJsonc logic locally to test it in isolation +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + +function patchJsonc(input: string, patch: unknown, path: string[] = []): string { + if (!isRecord(patch)) { + const edits = modify(input, path, patch === null ? undefined : patch, { + formattingOptions: { insertSpaces: true, tabSize: 2 }, + }) + return applyEdits(input, edits) + } + + if (path.length > 0) { + const tree = parseTree(input) + const node = tree && findNodeAtLocation(tree, path) + if (node && node.type !== "object") { + const edits = modify(input, path, patch, { + formattingOptions: { insertSpaces: true, tabSize: 2 }, + }) + return applyEdits(input, edits) + } + } + + return Object.entries(patch).reduce((result, [key, value]) => { + if (value === undefined) return result + return patchJsonc(result, value, [...path, key]) + }, input) +} + +describe("patchJsonc scalar-to-object transition", () => { + test("sets object when node does not yet exist", () => { + const input = `{ "permission": {} }` + const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } }) + const parsed = JSON.parse(result) + expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" }) + }) + + test("transitions permission rule from string to object (the bug scenario)", () => { + // bash is stored as a plain string "ask" in the JSONC file + const input = `{ "permission": { "bash": "ask" } }` + // User adds exception "uname" → "allow"; this would previously throw: + // "Can not add index to parent of type string" + const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } }) + const parsed = JSON.parse(result) + expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" }) + }) + + test("does not disturb sibling keys when replacing scalar", () => { + const input = `{ "permission": { "bash": "ask", "glob": "allow" } }` + const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } }) + const parsed = JSON.parse(result) + expect(parsed.permission.glob).toBe("allow") + expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" }) + }) + + test("updates existing object permission by adding a new key", () => { + const input = `{ "permission": { "bash": { "*": "ask" } } }` + const result = patchJsonc(input, { permission: { bash: { "*": "ask", uname: "allow" } } }) + const parsed = JSON.parse(result) + expect(parsed.permission.bash).toEqual({ "*": "ask", uname: "allow" }) + }) + + test("plain string permission update still works", () => { + const input = `{ "permission": { "bash": "allow" } }` + const result = patchJsonc(input, { permission: { bash: "ask" } }) + const parsed = JSON.parse(result) + expect(parsed.permission.bash).toBe("ask") + }) +})