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/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": { diff --git a/nix/hashes.json b/nix/hashes.json index 054cc05b35e..6ea6b6b344a 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-AIHdAoZ6f1J9uNuZ36X493dVLukXZ6e2SS4Y4fFSOGQ=", - "aarch64-linux": "sha256-o5aQJgSXEDF/hp8WvnhnTjYhg8uzNf2QFspOyH8HnCc=", - "aarch64-darwin": "sha256-086kLWpb1NNZGngb6YWMJlFtuJeMihTOWr1etS21xFU=", - "x86_64-darwin": "sha256-Exz71XKxPeAHutx7w8B5DhgoF9J/G5ZLqq4GrwbCzZk=" + "x86_64-linux": "sha256-2SnZbJPIvgCkb4jNh49CnbDAOpghgO9XihSiTlL6rYI=", + "aarch64-linux": "sha256-HV/Sm4rBAZPjxG+SQJ95634yTyJXDjlSeYemMYg8eCE=", + "aarch64-darwin": "sha256-PAJSd1VkNc7LUSrOAeG+rmYUw8VefR8GfkAIy8H3iV0=", + "x86_64-darwin": "sha256-Qdk8nu4FoGjp6+7Ji5vuHw2HhM1Z+ivE7meqJivTiHk=" } } 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. 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/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..bcd8cc2e1cc 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) => {
- - -
@@ -151,9 +162,7 @@ export const ChatView: Component = (props) => {
- - - +
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..419470c3afd 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" @@ -77,7 +78,6 @@ export const MessageList: Component = (props) => {
= (props) => { onClick={() => autoScroll.resume()} aria-label={language.t("session.messages.scrollToBottom")} > - ↓ +
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..de07923cfbd 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, createEffect, 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,527 @@ const LEVEL_OPTIONS: LevelOption[] = [ { value: "deny", labelKey: "settings.autoApprove.level.deny" }, ] +interface GranularConfig { + wildcardKey: string + addKey: string + placeholderKey: string +} + +interface ToolDef { + id: string + descriptionKey: string + granular?: GranularConfig +} + +interface GranularToolDef extends ToolDef { + granular: GranularConfig +} + +/** Grouped tool: maps a single UI row to multiple config keys */ +interface GroupedToolDef { + ids: string[] + label: string + descriptionKey: string +} + +const GRANULAR_TOOLS: GranularToolDef[] = [ + { + 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" }, +] + +/** + * Backend default permission levels — mirrors the base defaults defined in + * packages/opencode/src/agent/agent.ts (lines 61-78). The global default + * is "*": "allow"; these are the per-tool overrides. If the backend defaults + * change, this map must be updated to match. + */ +const TOOL_DEFAULTS: Partial> = { + doom_loop: "ask", + external_directory: "ask", +} + +const RESTRICTION_ORDER: Record = { allow: 0, ask: 1, deny: 2 } + +/** For grouped tools, return the most restrictive level across all IDs. */ +function mostRestrictive(levels: PermissionLevel[]): PermissionLevel { + return levels.reduce( + (best, l) => (RESTRICTION_ORDER[l] > RESTRICTION_ORDER[best] ? l : best), + levels[0] ?? "allow", + ) +} + +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 })) +} + +function toolTitle(id: string): string { + return id + .split("_") + .map((word) => word[0].toUpperCase() + word.slice(1)) + .join(" ") + .split(" / ") + .map((part) => part[0].toUpperCase() + part.slice(1)) + .join(" / ") +} + 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 "allow" // backend default: "*": "allow" (agent.ts) + }) + + const defaultFor = (tool: string): PermissionLevel => TOOL_DEFAULTS[tool] ?? globalFallback() + + const levelFor = (tool: string): PermissionLevel => wildcardAction(permissions()[tool], defaultFor(tool)) + + 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 fallback = defaultFor(tool) + const value: PermissionRule = + keys.length === 0 ? fallback : 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 plain string + // (which replaces the entire JSONC node), then immediately send the rebuilt object + // in a second call only when necessary. + // Both messages are dispatched synchronously before any reactive flush, so the + // second call always operates on the value we just computed — not on stale signal + // state — avoiding a race condition. + // Ideally when keys.length === 0 we'd remove the tool key entirely so it + // inherits the global default, but that requires backend support for null + // delete sentinels (tracked in #6625). + const wildcard = rebuilt["*"] ?? fallback + // Single call covers string and collapsed-to-string cases + updateConfig({ permission: { [tool]: wildcard } }) + // Only send the second call when the result must remain an object + if (typeof value === "object") { + // This runs synchronously in the same microtask tick; the first updateConfig + // queues a JSONC node replacement, and this one immediately overwrites it with + // the full object — no intervening reactive update occurs. + 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 ( +
+
+
{toolTitle(props.id)}
+
+ {language.t(props.descriptionKey)} +
+
+ +
+ ) +} + +const GranularToolRow: Component<{ + tool: GranularToolDef + 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("") + let inputRef: HTMLInputElement | undefined + + createEffect(() => { + if (adding()) inputRef?.focus() + }) + + 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 */} +
+
+
{toolTitle(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)} + + } + > +
+ (inputRef = el)} + 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 ( +