diff --git a/kolosal-server b/kolosal-server index dca2bd5..b0993f4 160000 --- a/kolosal-server +++ b/kolosal-server @@ -1 +1 @@ -Subproject commit dca2bd59a3d1e90edbca2309c092ca218d723310 +Subproject commit b0993f4ccdca3cf99adb12c486100e78da3408e3 diff --git a/package-lock.json b/package-lock.json index c77211b..2e566c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -375,6 +375,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -398,6 +399,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1638,6 +1640,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", @@ -1960,6 +1963,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3165,8 +3169,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/body-parser": { "version": "1.19.6", @@ -3456,6 +3459,7 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3466,6 +3470,7 @@ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -3633,6 +3638,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -4246,7 +4252,6 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", - "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -4260,7 +4265,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -4270,7 +4274,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -4283,6 +4286,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4493,8 +4497,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.9", @@ -4782,7 +4785,6 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -4807,7 +4809,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -4817,7 +4818,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -4829,15 +4829,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/body-parser/node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -5462,7 +5460,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -5501,8 +5498,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.3", @@ -5867,7 +5863,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -5923,8 +5918,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -6402,6 +6396,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6838,7 +6833,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -6900,7 +6894,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -6910,7 +6903,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -6919,15 +6911,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/express/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -7102,7 +7092,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -7121,7 +7110,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -7130,15 +7118,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/finalhandler/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -7254,7 +7240,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -8131,6 +8116,7 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz", "integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", @@ -9104,6 +9090,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -9483,7 +9470,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9565,7 +9551,6 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -9604,7 +9589,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -9624,7 +9608,6 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -9774,6 +9757,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", @@ -9927,7 +9911,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -10810,8 +10793,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", @@ -11182,7 +11164,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "side-channel": "^1.0.6" }, @@ -11292,6 +11273,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11302,6 +11284,7 @@ "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -11335,6 +11318,7 @@ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -11912,7 +11896,6 @@ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -11937,7 +11920,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -11946,15 +11928,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -11964,7 +11944,6 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", - "peer": true, "bin": { "mime": "cli.js" }, @@ -11977,7 +11956,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -11987,7 +11965,6 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", - "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -13010,6 +12987,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13183,7 +13161,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -13248,7 +13227,6 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", - "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -13262,7 +13240,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -13272,7 +13249,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -13364,6 +13340,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13682,7 +13659,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -13725,6 +13701,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -14238,6 +14215,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -14820,6 +14798,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -14902,6 +14881,7 @@ "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -15226,7 +15206,6 @@ "version": "10.4.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -15247,7 +15226,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -15289,7 +15267,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -15300,7 +15277,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -15312,7 +15288,6 @@ "version": "5.3.0", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -15326,8 +15301,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "packages/cli/node_modules/string-width": { "version": "7.2.0", diff --git a/packages/cli/src/config/savedModels.test.ts b/packages/cli/src/config/savedModels.test.ts index e9ecc6e..f076805 100644 --- a/packages/cli/src/config/savedModels.test.ts +++ b/packages/cli/src/config/savedModels.test.ts @@ -10,6 +10,7 @@ import { findKolosalApiKey, getCurrentModelAuthType, mergeSavedModelEntries, + removeSavedModelEntry, upsertSavedModelEntry, type SavedModelEntry, } from './savedModels.js'; @@ -249,3 +250,103 @@ describe('findKolosalApiKey', () => { ).toBeUndefined(); }); }); + +describe('removeSavedModelEntry', () => { + it('removes a model entry by key', () => { + const existing: SavedModelEntry[] = [ + { + id: 'model-1', + provider: 'openai-compatible', + baseUrl: 'https://api.example.com/v1', + }, + { + id: 'model-2', + provider: 'openai-compatible', + baseUrl: 'https://api.example.com/v1', + }, + ]; + + const removed = removeSavedModelEntry(existing, { + id: 'model-1', + provider: 'openai-compatible', + baseUrl: 'https://api.example.com/v1', + }); + + expect(removed).toHaveLength(1); + expect(removed[0]?.id).toBe('model-2'); + }); + + it('handles empty array', () => { + const removed = removeSavedModelEntry([], { + id: 'model-1', + provider: 'openai-compatible', + }); + + expect(removed).toHaveLength(0); + }); + + it('handles undefined input', () => { + const removed = removeSavedModelEntry(undefined, { + id: 'model-1', + provider: 'openai-compatible', + }); + + expect(removed).toHaveLength(0); + }); + + it('handles non-existent model', () => { + const existing: SavedModelEntry[] = [ + { + id: 'model-1', + provider: 'openai-compatible', + }, + ]; + + const removed = removeSavedModelEntry(existing, { + id: 'model-2', + provider: 'openai-compatible', + }); + + expect(removed).toHaveLength(1); + expect(removed[0]?.id).toBe('model-1'); + }); + + it('removes oss-local models correctly', () => { + const existing: SavedModelEntry[] = [ + { + id: 'unsloth/Qwen3-1.7B', + provider: 'oss-local', + }, + { + id: 'meta/llama', + provider: 'oss-local', + }, + ]; + + const removed = removeSavedModelEntry(existing, { + id: 'unsloth/Qwen3-1.7B', + provider: 'oss-local', + }); + + expect(removed).toHaveLength(1); + expect(removed[0]?.id).toBe('meta/llama'); + }); + + it('normalizes baseUrl when matching', () => { + const existing: SavedModelEntry[] = [ + { + id: 'model-1', + provider: 'openai-compatible', + baseUrl: 'https://api.example.com/v1/', + }, + ]; + + const removed = removeSavedModelEntry(existing, { + id: 'model-1', + provider: 'openai-compatible', + baseUrl: 'https://api.example.com/v1', + }); + + expect(removed).toHaveLength(0); + }); +}); diff --git a/packages/cli/src/config/savedModels.ts b/packages/cli/src/config/savedModels.ts index 8c7ec6c..253a1be 100644 --- a/packages/cli/src/config/savedModels.ts +++ b/packages/cli/src/config/savedModels.ts @@ -74,6 +74,22 @@ export function upsertSavedModelEntry( return [...filtered, normalizedEntry]; } +export function removeSavedModelEntry( + existing: SavedModelEntry[] | undefined, + entry: SavedModelEntry, +): SavedModelEntry[] { + const list = existing ?? []; + if (list.length === 0) { + return []; + } + const normalizedEntry = sanitizeEntry(entry); + const keyToRemove = keyFor(normalizedEntry); + return list.filter((item) => { + const normalizedItem = sanitizeEntry(item); + return keyFor(normalizedItem) !== keyToRemove; + }); +} + export function mergeSavedModelEntries( sources: Array, ): SavedModelEntry[] { diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 6163538..0395601 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -27,6 +27,7 @@ import { initCommand } from '../ui/commands/initCommand.js'; import { mcpCommand } from '../ui/commands/mcpCommand.js'; import { memoryCommand } from '../ui/commands/memoryCommand.js'; import { modelCommand } from '../ui/commands/modelCommand.js'; +import { modelDeleteCommand } from '../ui/commands/modelDeleteCommand.js'; import { privacyCommand } from '../ui/commands/privacyCommand.js'; import { quitCommand, quitConfirmCommand } from '../ui/commands/quitCommand.js'; import { restoreCommand } from '../ui/commands/restoreCommand.js'; @@ -75,6 +76,7 @@ export class BuiltinCommandLoader implements ICommandLoader { mcpCommand, memoryCommand, modelCommand, + modelDeleteCommand, privacyCommand, quitCommand, quitConfirmCommand, diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index ac0b078..81d2e7d 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -62,6 +62,7 @@ export const createMockCommandContext = ( session: { sessionShellAllowlist: new Set(), stats: { + sessionId: 'test-session-id', sessionStartTime: new Date(), lastPromptTokenCount: 0, metrics: { diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index f111006..ab32b71 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -58,6 +58,10 @@ import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js import { QuitConfirmationDialog } from './components/QuitConfirmationDialog.js'; import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js'; import { ModelSelectionDialog } from './components/ModelSelectionDialog.js'; +import { + ModelDeleteConfirmationDialog, + ModelDeleteChoice, +} from './components/ModelDeleteConfirmationDialog.js'; import { HfModelPickerDialog } from './components/HfModelPickerDialog.js'; import { HfModelFilePickerDialog, @@ -145,6 +149,7 @@ import { useKittyKeyboardProtocol } from './hooks/useKittyKeyboardProtocol.js'; import { keyMatchers, Command } from './keyMatchers.js'; import { upsertSavedModelEntry, + removeSavedModelEntry, getCurrentModelAuthType, deriveOpenAIEnvConfig, findKolosalApiKey, @@ -328,6 +333,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { // Model selection dialog states const [isModelSelectionDialogOpen, setIsModelSelectionDialogOpen] = useState(false); + const [isModelDeleteDialogOpen, setIsModelDeleteDialogOpen] = useState(false); + const [modelToDelete, setModelToDelete] = useState(null); const [isHfPickerOpen, setIsHfPickerOpen] = useState(false); const [isHfFilePickerOpen, setIsHfFilePickerOpen] = useState(false); const [selectedHfModelId, setSelectedHfModelId] = useState(null); @@ -2139,6 +2146,132 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { setIsModelSelectionDialogOpen(false); }, []); + const handleModelDeleteOpen = useCallback(() => { + // Open model selection dialog filtered to deletable models + setIsModelSelectionDialogOpen(true); + }, []); + + const handleModelDeleteClose = useCallback(() => { + setIsModelDeleteDialogOpen(false); + setModelToDelete(null); + }, []); + + const handleModelDeleteRequest = useCallback( + (model: AvailableModel) => { + const runtimeId = model.runtimeId ?? model.id; + const isCurrentModel = runtimeId === currentModel; + if (isCurrentModel) { + addItem( + { + type: MessageType.ERROR, + text: 'Cannot delete the currently active model. Switch to a different model first.', + }, + Date.now(), + ); + return; + } + if (!model.savedModel) { + addItem( + { + type: MessageType.ERROR, + text: 'This model cannot be deleted as it is not a saved custom model.', + }, + Date.now(), + ); + return; + } + // Prevent deletion of Kolosal Cloud models + const isKolosalCloudModel = + model.savedModel.id.startsWith('kolosal-') || + model.label?.endsWith('(Kolosal Cloud)') || + model.savedModel.baseUrl === KOLOSAL_API_BASE_URL; + if (isKolosalCloudModel) { + addItem( + { + type: MessageType.ERROR, + text: 'Cannot delete Kolosal Cloud models. These are managed by Kolosal and cannot be removed.', + }, + Date.now(), + ); + return; + } + setModelToDelete(model); + setIsModelDeleteDialogOpen(true); + }, + [currentModel, addItem], + ); + + const handleModelDeleteConfirm = useCallback( + (choice: ModelDeleteChoice) => { + if (choice === ModelDeleteChoice.CANCEL) { + handleModelDeleteClose(); + return; + } + + if (choice === ModelDeleteChoice.DELETE && modelToDelete?.savedModel) { + // Double-check: prevent deletion of Kolosal Cloud models as a safety measure + const isKolosalCloudModel = + modelToDelete.savedModel.id.startsWith('kolosal-') || + modelToDelete.label?.endsWith('(Kolosal Cloud)') || + modelToDelete.savedModel.baseUrl === KOLOSAL_API_BASE_URL; + if (isKolosalCloudModel) { + addItem( + { + type: MessageType.ERROR, + text: 'Cannot delete Kolosal Cloud models. These are managed by Kolosal and cannot be removed.', + }, + Date.now(), + ); + handleModelDeleteClose(); + return; + } + + const modelLabel = modelToDelete.label ?? modelToDelete.id; + const existingSavedModels = ( + settings.merged.model?.savedModels ?? [] + ) as SavedModelEntry[]; + + // Find the exact model to delete by matching both id and runtimeModelId + // to prevent accidentally deleting the wrong model + const modelToDeleteId = modelToDelete.savedModel.id; + const modelToDeleteRuntimeId = modelToDelete.savedModel.runtimeModelId; + const modelToDeleteBaseUrl = modelToDelete.savedModel.baseUrl; + const modelToDeleteProvider = modelToDelete.savedModel.provider; + + const updatedModels = existingSavedModels.filter((entry) => { + // Match by exact id, provider, and baseUrl to ensure we delete the correct model + const idMatches = entry.id === modelToDeleteId; + const providerMatches = entry.provider === modelToDeleteProvider; + const baseUrlMatches = + (entry.baseUrl ?? '') === (modelToDeleteBaseUrl ?? ''); + const runtimeIdMatches = + !modelToDeleteRuntimeId || + entry.runtimeModelId === modelToDeleteRuntimeId; + + // Only delete if all identifiers match + return !(idMatches && providerMatches && baseUrlMatches && runtimeIdMatches); + }); + + settings.setValue( + SettingScope.User, + 'model.savedModels', + updatedModels, + ); + + addItem( + { + type: MessageType.INFO, + text: `Model \`${modelLabel}\` has been deleted.`, + }, + Date.now(), + ); + + handleModelDeleteClose(); + } + }, + [modelToDelete, settings, addItem, handleModelDeleteClose], + ); + const handleModelSelect = useCallback( async (model: AvailableModel) => { const savedModelId = model.savedModel?.id ?? model.id; @@ -2417,6 +2550,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { openPrivacyNotice, openSettingsDialog, handleModelSelectionOpen, + handleModelDeleteOpen, openSubagentCreateDialog, openAgentsManagerDialog, toggleVimEnabled, @@ -3252,6 +3386,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { currentModel={currentModel} onSelect={handleModelSelect} onCancel={handleModelSelectionClose} + onDelete={handleModelDeleteRequest} + /> + ) : isModelDeleteDialogOpen && modelToDelete ? ( + ) : isVisionSwitchDialogOpen ? ( diff --git a/packages/cli/src/ui/commands/modelDeleteCommand.test.ts b/packages/cli/src/ui/commands/modelDeleteCommand.test.ts new file mode 100644 index 0000000..d6fc069 --- /dev/null +++ b/packages/cli/src/ui/commands/modelDeleteCommand.test.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2025 Kolosal + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { modelDeleteCommand } from './modelDeleteCommand.js'; +import { AuthType } from '@kolosal-ai/kolosal-ai-core'; +import type { CommandContext } from './types.js'; +import type { SavedModelEntry } from '../../config/savedModels.js'; +import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; + +describe('modelDeleteCommand', () => { + let mockContext: CommandContext; + const mockGetModel = vi.fn(() => 'current-model'); + + beforeEach(() => { + mockContext = createMockCommandContext({ + services: { + config: { + getModel: mockGetModel, + getContentGeneratorConfig: vi.fn(() => ({ + authType: AuthType.USE_OPENAI, + model: 'current-model', + })), + } as any, + settings: { + merged: { + model: { + savedModels: [] as SavedModelEntry[], + }, + }, + } as any, + }, + }); + }); + + it('has correct name and description', () => { + expect(modelDeleteCommand.name).toBe('model-delete'); + expect(modelDeleteCommand.altNames).toContain('delete-model'); + expect(modelDeleteCommand.description).toBe('Delete a saved custom model'); + }); + + it('returns error when config is not available', async () => { + const contextWithoutConfig = { + ...mockContext, + services: { + ...mockContext.services, + config: null, + }, + }; + + const result = await modelDeleteCommand.action!(contextWithoutConfig, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Configuration not available.', + }); + }); + + it('returns error when no saved models exist', async () => { + const result = await modelDeleteCommand.action!(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'No saved models found. Nothing to delete.', + }); + }); + + it('returns error when only current model exists', async () => { + if (mockContext.services.settings) { + mockContext.services.settings.merged.model = { + savedModels: [ + { + id: 'current-model', + provider: 'openai-compatible', + }, + ] as SavedModelEntry[], + }; + } + + const result = await modelDeleteCommand.action!(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: + 'No deletable models found. The currently active model cannot be deleted. Switch to a different model first.', + }); + }); + + it('opens model_delete dialog when deletable models exist', async () => { + if (mockContext.services.settings) { + mockContext.services.settings.merged.model = { + savedModels: [ + { + id: 'current-model', + provider: 'openai-compatible', + }, + { + id: 'deletable-model', + provider: 'openai-compatible', + }, + ] as SavedModelEntry[], + }; + } + + const result = await modelDeleteCommand.action!(mockContext, ''); + + expect(result).toEqual({ + type: 'dialog', + dialog: 'model_delete', + }); + }); + + it('handles models with runtimeModelId', async () => { + mockGetModel.mockReturnValue('runtime-model-id'); + if (mockContext.services.settings) { + mockContext.services.settings.merged.model = { + savedModels: [ + { + id: 'saved-model-id', + provider: 'openai-compatible', + runtimeModelId: 'runtime-model-id', + }, + { + id: 'deletable-model', + provider: 'openai-compatible', + }, + ] as SavedModelEntry[], + }; + } + + const result = await modelDeleteCommand.action!(mockContext, ''); + + expect(result).toEqual({ + type: 'dialog', + dialog: 'model_delete', + }); + }); +}); + diff --git a/packages/cli/src/ui/commands/modelDeleteCommand.ts b/packages/cli/src/ui/commands/modelDeleteCommand.ts new file mode 100644 index 0000000..7123778 --- /dev/null +++ b/packages/cli/src/ui/commands/modelDeleteCommand.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright 2025 Kolosal + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + SlashCommand, + CommandContext, + OpenDialogActionReturn, + MessageActionReturn, +} from './types.js'; +import { CommandKind } from './types.js'; +import type { SavedModelEntry } from '../../config/savedModels.js'; + +const KOLOSAL_API_BASE_URL = 'https://api.kolosal.ai/v1'; + +function isKolosalCloudModel(entry: SavedModelEntry): boolean { + return ( + entry.id.startsWith('kolosal-') || + entry.label?.endsWith('(Kolosal Cloud)') || + entry.baseUrl === KOLOSAL_API_BASE_URL + ); +} + +export const modelDeleteCommand: SlashCommand = { + name: 'model-delete', + altNames: ['delete-model'], + description: 'Delete a saved custom model', + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + ): Promise => { + const { services } = context; + const { config, settings } = services; + + if (!config) { + return { + type: 'message', + messageType: 'error', + content: 'Configuration not available.', + }; + } + + const savedModels = + (settings.merged.model?.savedModels ?? []) as SavedModelEntry[]; + + if (!savedModels || savedModels.length === 0) { + return { + type: 'message', + messageType: 'error', + content: 'No saved models found. Nothing to delete.', + }; + } + + const currentModel = config.getModel(); + const deletableModels = savedModels.filter((entry) => { + // Exclude Kolosal Cloud models + if (isKolosalCloudModel(entry)) { + return false; + } + // Exclude currently active model + const runtimeId = entry.runtimeModelId ?? entry.id; + return runtimeId !== currentModel; + }); + + if (deletableModels.length === 0) { + return { + type: 'message', + messageType: 'error', + content: + 'No deletable models found. The currently active model cannot be deleted. Switch to a different model first.', + }; + } + + // Trigger model delete selection dialog + return { + type: 'dialog', + dialog: 'model_delete', + }; + }, +}; + diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts index 74e04a1..0720912 100644 --- a/packages/cli/src/ui/commands/types.ts +++ b/packages/cli/src/ui/commands/types.ts @@ -117,6 +117,7 @@ export interface OpenDialogActionReturn { | 'privacy' | 'settings' | 'model' + | 'model_delete' | 'subagent_create' | 'subagent_list'; } diff --git a/packages/cli/src/ui/components/ModelDeleteConfirmationDialog.tsx b/packages/cli/src/ui/components/ModelDeleteConfirmationDialog.tsx new file mode 100644 index 0000000..e2f142f --- /dev/null +++ b/packages/cli/src/ui/components/ModelDeleteConfirmationDialog.tsx @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2025 Kolosal + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Box, Text } from 'ink'; +import type React from 'react'; +import { Colors } from '../colors.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './shared/RadioButtonSelect.js'; +import { useKeypress } from '../hooks/useKeypress.js'; +import { LeftBorderPanel } from './shared/LeftBorderPanel.js'; +import { getPanelBackgroundColor } from './shared/panelStyles.js'; +import type { AvailableModel } from '../models/availableModels.js'; + +export enum ModelDeleteChoice { + CANCEL = 'cancel', + DELETE = 'delete', +} + +export interface ModelDeleteConfirmationDialogProps { + model: AvailableModel; + onSelect: (choice: ModelDeleteChoice) => void; +} + +export const ModelDeleteConfirmationDialog: React.FC< + ModelDeleteConfirmationDialogProps +> = ({ model, onSelect }) => { + useKeypress( + (key) => { + if (key.name === 'escape') { + onSelect(ModelDeleteChoice.CANCEL); + } + }, + { isActive: true }, + ); + + const modelLabel = model.label ?? model.id; + const options: Array> = [ + { + label: 'Yes, delete', + value: ModelDeleteChoice.DELETE, + }, + { + label: 'Cancel (Esc)', + value: ModelDeleteChoice.CANCEL, + }, + ]; + + return ( + + + Delete Model + + Are you sure you want to delete the model "{modelLabel}"? + + + + This action cannot be undone. + + + + + + + ); +}; + diff --git a/packages/cli/src/ui/components/ModelSelectionDialog.tsx b/packages/cli/src/ui/components/ModelSelectionDialog.tsx index 5c488d2..3bf0629 100644 --- a/packages/cli/src/ui/components/ModelSelectionDialog.tsx +++ b/packages/cli/src/ui/components/ModelSelectionDialog.tsx @@ -59,6 +59,7 @@ export interface ModelSelectionDialogProps { currentModel: string; onSelect: (model: AvailableModel) => void; onCancel: () => void; + onDelete?: (model: AvailableModel) => void; } export const ModelSelectionDialog: React.FC = ({ @@ -66,9 +67,11 @@ export const ModelSelectionDialog: React.FC = ({ currentModel, onSelect, onCancel, + onDelete, }) => { const { columns } = useTerminalSize(); const [spinnerIndex, setSpinnerIndex] = useState(0); + const [selectedIndex, setSelectedIndex] = useState(0); useEffect(() => { const interval = setInterval(() => { @@ -83,6 +86,12 @@ export const ModelSelectionDialog: React.FC = ({ (key) => { if (key.name === 'escape') { onCancel(); + } else if (key.name === 'd' && onDelete) { + const selectedModel = availableModels[selectedIndex]; + if (selectedModel && selectedModel.savedModel) { + // Let handleModelDeleteRequest handle the current model check and show error + onDelete(selectedModel); + } } }, { isActive: true }, @@ -187,10 +196,23 @@ export const ModelSelectionDialog: React.FC = ({ ), ); + useEffect(() => { + setSelectedIndex(initialIndex); + }, [initialIndex]); + const handleSelect = (model: AvailableModel) => { onSelect(model); }; + const handleHighlight = (model: AvailableModel) => { + const index = availableModels.findIndex( + (m) => (m.runtimeId ?? m.id) === (model.runtimeId ?? model.id), + ); + if (index >= 0) { + setSelectedIndex(index); + } + }; + return ( = ({ items={options} initialIndex={initialIndex} onSelect={handleSelect} + onHighlight={handleHighlight} isFocused /> - Press Enter to select, Esc to cancel + + Press Enter to select{onDelete ? ", 'd' to delete" : ''}, Esc to cancel + ); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 69a917c..a3cf2b6 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -54,6 +54,7 @@ export const useSlashCommandProcessor = ( openPrivacyNotice: () => void, openSettingsDialog: () => void, openModelSelectionDialog: () => void, + openModelDeleteDialog: () => void, openSubagentCreateDialog: () => void, openAgentsManagerDialog: () => void, toggleVimEnabled: () => Promise, @@ -408,6 +409,9 @@ export const useSlashCommandProcessor = ( case 'model': openModelSelectionDialog(); return { type: 'handled' }; + case 'model_delete': + openModelDeleteDialog(); + return { type: 'handled' }; case 'subagent_create': openSubagentCreateDialog(); return { type: 'handled' };