diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index d6a323be..73961d97 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -26,16 +26,19 @@ "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "2.4.1", "autoprefixer": "^10.4.20", + "diff-match-patch": "^1.0.5", "esbuild": "^0.23.1", "eslint": "^8.57.0", "eslint-plugin-react": "^7.35.1", "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", "marked": "^14.1.0", + "ollama": "^0.5.9", "postcss": "^8.4.41", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.6.1", "rimraf": "^6.0.1", "tailwindcss": "^3.4.10", "typescript": "5.5.4", @@ -197,6 +200,19 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -2151,6 +2167,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2843,6 +2866,20 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3028,6 +3065,15 @@ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", "license": "MIT" }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", @@ -3328,6 +3374,17 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", @@ -3370,6 +3427,77 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3380,6 +3508,23 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4406,6 +4551,21 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5557,6 +5717,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ollama": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz", + "integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-fetch": "^3.6.20" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6173,6 +6343,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6311,6 +6491,24 @@ "react": ">=18" } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6372,6 +6570,139 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -7769,6 +8100,13 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -8010,6 +8348,16 @@ "dev": true, "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 7b876946..de2c5312 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -15,254 +15,7 @@ "contributes": { "configuration": { "title": "Void", - "properties": { - "void.whichApi": { - "type": "string", - "default": "anthropic", - "description": "Choose an API provider.", - "enum": [ - "openAI", - "openRouter", - "openAICompatible", - "anthropic", - "azure", - "ollama", - "greptile" - ] - }, - "void.anthropic.apiKey": { - "type": "string", - "default": "", - "description": "Anthropic API key." - }, - "void.anthropic.model": { - "type": "string", - "default": "claude-3-5-sonnet-20240620", - "description": "Anthropic model to use.", - "enum": [ - "claude-3-5-sonnet-20240620", - "claude-3-opus-20240229", - "claude-3-sonnet-20240229", - "claude-3-haiku-20240307" - ] - }, - "void.anthropic.maxTokens": { - "type": "string", - "default": "8192", - "description": "Anthropic max number of tokens to output.", - "enum": [ - "1024", - "2048", - "4096", - "8192" - ] - }, - "void.openAI.apiKey": { - "type": "string", - "default": "", - "description": "OpenAI API key." - }, - "void.openAI.model": { - "type": "string", - "default": "gpt-4o", - "description": "OpenAI model to use.", - "enum": [ - "o1-preview", - "o1-mini", - "gpt-4o", - "gpt-4o-2024-05-13", - "gpt-4o-2024-08-06", - "gpt-4o-mini", - "gpt-4o-mini-2024-07-18", - "gpt-4-turbo", - "gpt-4-turbo-2024-04-09", - "gpt-4-turbo-preview", - "gpt-4-0125-preview", - "gpt-4-1106-preview", - "gpt-4", - "gpt-4-0613", - "gpt-3.5-turbo-0125", - "gpt-3.5-turbo", - "gpt-3.5-turbo-1106" - ] - }, - "void.greptile.apiKey": { - "type": "string", - "default": "", - "description": "Greptile API key." - }, - "void.greptile.githubPAT": { - "type": "string", - "default": "", - "description": "Github PAT given to Greptile to access your repository." - }, - "void.greptile.remote": { - "type": "string", - "description": "remote provider", - "enum": [ - "github", - "gitlab" - ] - }, - "void.greptile.repository": { - "type": "string", - "description": "Repository identifier in \"owner/repository\" format." - }, - "void.greptile.branch": { - "type": "string", - "default": "main", - "description": "Name of the git branch." - }, - "void.azure.apiKey": { - "type": "string", - "description": "Azure API key." - }, - "void.azure.deploymentId": { - "type": "string", - "description": "Azure API deployment ID." - }, - "void.azure.resourceName": { - "type": "string", - "description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`" - }, - "void.azure.providerSettings": { - "type": "object", - "properties": { - "baseURL": { - "type": "string", - "default": "https://${resourceName}.openai.azure.com/openai/deployments", - "description": "Azure API base URL." - }, - "headers": { - "type": "object", - "description": "Custom headers to include in the requests." - } - } - }, - "void.ollama.endpoint": { - "type": "string", - "default": "http://127.0.0.1:11434", - "description": "The Ollama endpoint. Start Ollama by running `OLLAMA_ORIGINS=\"vscode-webview://*\" ollama serve`" - }, - "void.ollama.model": { - "type": "string", - "default": "llama3.1", - "description": "Ollama model to use.", - "enum": [ - "codegemma", - "codegemma:2b", - "codegemma:7b", - "codellama", - "codellama:7b", - "codellama:13b", - "codellama:34b", - "codellama:70b", - "codellama:code", - "codellama:python", - "command-r", - "command-r:35b", - "command-r-plus", - "command-r-plus:104b", - "deepseek-coder-v2", - "deepseek-coder-v2:16b", - "deepseek-coder-v2:236b", - "falcon2", - "falcon2:11b", - "firefunction-v2", - "firefunction-v2:70b", - "gemma", - "gemma:2b", - "gemma:7b", - "gemma2", - "gemma2:2b", - "gemma2:9b", - "gemma2:27b", - "llama2", - "llama2:7b", - "llama2:13b", - "llama2:70b", - "llama3", - "llama3:8b", - "llama3:70b", - "llama3-chatqa", - "llama3-chatqa:8b", - "llama3-chatqa:70b", - "llama3-gradient", - "llama3-gradient:8b", - "llama3-gradient:70b", - "llama3.1", - "llama3.1:8b", - "llama3.1:70b", - "llama3.1:405b", - "llava", - "llava:7b", - "llava:13b", - "llava:34b", - "llava-llama3", - "llava-llama3:8b", - "llava-phi3", - "llava-phi3:3.8b", - "mistral", - "mistral:7b", - "mistral-large", - "mistral-large:123b", - "mistral-nemo", - "mistral-nemo:12b", - "mixtral", - "mixtral:8x7b", - "mixtral:8x22b", - "moondream", - "moondream:1.8b", - "openhermes", - "openhermes:v2.5", - "phi3", - "phi3:3.8b", - "phi3:14b", - "phi3.5", - "phi3.5:3.8b", - "qwen", - "qwen:7b", - "qwen:14b", - "qwen:32b", - "qwen:72b", - "qwen:110b", - "qwen2", - "qwen2:0.5b", - "qwen2:1.5b", - "qwen2:7b", - "qwen2:72b", - "smollm", - "smollm:135m", - "smollm:360m", - "smollm:1.7b" - ] - }, - "void.openRouter.model": { - "type": "string", - "default": "openai/gpt-4o", - "description": "OpenRouter model to use." - }, - "void.openRouter.apiKey": { - "type": "string", - "default": "", - "description": "OpenRouter API key." - }, - "void.openAICompatible.endpoint": { - "type": "string", - "default": "http://127.0.0.1:11434/v1", - "description": "The endpoint." - }, - "void.openAICompatible.model": { - "type": "string", - "default": "gpt-4o", - "description": "The name of the model to use." - }, - "void.openAICompatible.apiKey": { - "type": "string", - "default": "", - "description": "Your API key." - } - } + "properties": {} }, "commands": [ { @@ -292,7 +45,7 @@ "icon": "$(history)" }, { - "command": "void.openSettings", + "command": "void.toggleSettings", "title": "Void settings", "icon": "$(settings-gear)" } @@ -340,7 +93,7 @@ "group": "navigation" }, { - "command": "void.openSettings", + "command": "void.toggleSettings", "when": "view == 'void.viewnumberone'", "group": "navigation" } @@ -369,16 +122,19 @@ "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "2.4.1", "autoprefixer": "^10.4.20", + "diff-match-patch": "^1.0.5", "esbuild": "^0.23.1", "eslint": "^8.57.0", "eslint-plugin-react": "^7.35.1", "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", "marked": "^14.1.0", + "ollama": "^0.5.9", "postcss": "^8.4.41", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.6.1", "rimraf": "^6.0.1", "tailwindcss": "^3.4.10", "typescript": "5.5.4", diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 4166d5b4..9394b410 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -28,7 +28,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { // used internally by vscode public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { const docUriStr = document.uri.toString() - return this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) + return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? [] } // declared by us, registered with vscode.languages.registerCodeLensProvider() diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts index a6cdf85c..c8aaffba 100644 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ b/extensions/void/src/SidebarWebviewProvider.ts @@ -19,7 +19,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { private readonly _extensionUri: vscode.Uri - private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration + // private _webviewView?: vscode.WebviewView; private _webviewDeps: string[] = []; constructor(context: vscode.ExtensionContext) { @@ -28,35 +28,14 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { let temp_res: typeof this._res | undefined = undefined this.webview = new Promise((res, rej) => { temp_res = res }) - if (!temp_res) throw new Error("sidebar provider: resolver was undefined") + if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined") this._res = temp_res - - // if it affects one of the config items webview depends on, update the webview - // TODO should be able to move this entirely to React - make updateWebviewHTML mount once, and then send updates via postMessage from then on - vscode.workspace.onDidChangeConfiguration(event => { - if (this._webviewDeps.map(dep => event.affectsConfiguration(dep)).some(v => !!v)) { - if (this._webviewView) { - this.updateWebviewHTML(this._webviewView.webview); - } - } - }); } - // this is updated - private updateWebviewHTML(webview: vscode.Webview) { - const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com']; + // called by us + updateWebviewHTML(webview: vscode.Webview) { this._webviewDeps = [] - const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void.ollama').get('endpoint'); - this._webviewDeps.push('void.ollama.endpoint'); - if (ollamaEndpoint) - allowed_urls.push(ollamaEndpoint); - - const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint'); - this._webviewDeps.push('void.openAICompatible.endpoint'); - if (openAICompatibleEndpoint) - allowed_urls.push(openAICompatibleEndpoint+'/chat/completions'); - const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js')); const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css')); const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri)); @@ -68,7 +47,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { Custom View - + @@ -100,6 +79,6 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { // resolve webview and _webviewView this._res(webview); - this._webviewView = webviewView; + // this._webviewView = webviewView; } } diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index b505e6a2..a0ab0df3 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,436 +1,295 @@ -import Anthropic from '@anthropic-ai/sdk' +import Anthropic from '@anthropic-ai/sdk'; +import OpenAI from 'openai'; import { Ollama } from 'ollama/browser' -import OpenAI from 'openai' +import { VoidConfig } from '../sidebar/contextForConfig'; -// always compare these against package.json to make sure every setting in this type can actually be provided by the user -export type ApiConfig = { - anthropic: { - apikey: string, - model: string, - maxTokens: string - }, - openAI: { - apikey: string, - model: string - }, - greptile: { - apikey: string, - githubPAT: string, - repoinfo: { - remote: string, // e.g. 'github' - repository: string, // e.g. 'voideditor/void' - branch: string // e.g. 'main' - } - }, - ollama: { - endpoint: string, - model: string - }, - openAICompatible: { - endpoint: string, - model: string, - apikey: string - }, - openRouter: { - model: string, - apikey: string - } - whichApi: string -} -type OnText = (newText: string, fullText: string) => void; + + +type OnText = (newText: string, fullText: string) => void export type LLMMessage = { role: 'user' | 'assistant', content: string -}; +} type SendLLMMessageFnTypeInternal = (params: { messages: LLMMessage[], onText: OnText, onFinalMessage: (input: string) => void, - onError: (message: string) => void, - apiConfig: ApiConfig -}) => { - abort: () => void -}; + onError: (error: string) => void, + voidConfig: VoidConfig, +}) + => { + abort: () => void + } type SendLLMMessageFnTypeExternal = (params: { messages: LLMMessage[], onText: OnText, onFinalMessage: (input: string) => void, - onError: (message: string) => void, - apiConfig: ApiConfig | null -}) => { - abort: () => void -}; - -type AnthropicErrorResponse = { - type: string, - error: { - type: string, - message: string - }; -}; - -// Helper function to handle missing API keys -const handleMissingApiKey = (serviceName: string, onError: (message: string) => void) => { - onError(`${serviceName} API key not set`); - return { abort: () => {} } -}; - -// Claude -const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ - messages, - onText, - onFinalMessage, - onError, - apiConfig -}) => { - const { apikey, model, maxTokens } = apiConfig.anthropic; - - if (!apikey) { - return handleMissingApiKey('Anthropic', onError); + onError: (error: string) => void, + voidConfig: VoidConfig | null, +}) + => { + abort: () => void } - let didAbort = false; - const anthropic = new Anthropic({ - apiKey: apikey, - dangerouslyAllowBrowser: true, + + +// Anthropic +const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { + + const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] + + const stream = anthropic.messages.stream({ + model: voidConfig.anthropic.model, + max_tokens: parseInt(voidConfig.anthropic.maxTokens), + messages: messages, + }); + + let did_abort = false + + // when receive text + stream.on('text', (newText, fullText) => { + if (did_abort) return + onText(newText, fullText) }) - const stream = anthropic.messages - .stream({ - model: model, - max_tokens: parseInt(maxTokens), - messages: messages, - stream: true - }) - .on('error', (err) => { - if (err instanceof Anthropic.APIError) { - if (err.status === 401) { - onError('Unauthorized: Invalid Anthropic API key'); - } else { - onError((err.error as AnthropicErrorResponse).error.message); - } - } else { - console.error(err); - onError(err.message); - } - }) - .on('text', (newText, fullText) => { - if (didAbort) return; - onText(newText, fullText); - }) - .on('finalMessage', (claudeResponse) => { - if (didAbort) return; - const content = claudeResponse.content - .filter((c) => c.type === 'text') - .map((c) => c.text) - .join('\n'); - onFinalMessage(content); - }); + // when we get the final message on this stream (or when error/fail) + stream.on('finalMessage', (claude_response) => { + if (did_abort) return + // stringify the response's content + let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n'); + onFinalMessage(content) + }) + stream.on('error', (error) => { + // the most common error will be invalid API key (401), so we handle this with a nice message + if (error instanceof Anthropic.APIError && error.status === 401) { + onError('Invalid API key.') + } + else { + onError(error.message) + } + }) + + // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either const abort = () => { - stream.controller.abort(); - didAbort = true; - }; + // stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error + did_abort = true + } + + return { abort } - return { abort }; }; + + // OpenAI, OpenRouter, OpenAICompatible -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ - messages, - onText, - onFinalMessage, - onError, - apiConfig -}) => { - const { apikey, model } = apiConfig.openAI; +const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { + let didAbort = false + let fullText = '' - - let didAbort = false; - let fullText = ''; - - let abort = () => { + // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either + let abort: () => void = () => { didAbort = true; }; - let openai: OpenAI; - let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming; + let openai: OpenAI + let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming - - if (apiConfig.whichApi === 'openAI') { - if (!apikey) { - return handleMissingApiKey('OpenAI', onError); - } - openai = new OpenAI({ apiKey: apiConfig.openAI.apikey, dangerouslyAllowBrowser: true }); - options = { model: apiConfig.openAI.model, messages: messages, stream: true, }; + if (voidConfig.default.whichApi === 'openAI') { + openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true }); + options = { model: voidConfig.openAI.model, messages: messages, stream: true, } } - else if (apiConfig.whichApi === 'openRouter') { + else if (voidConfig.default.whichApi === 'openRouter') { openai = new OpenAI({ - baseURL: "https://openrouter.ai/api/v1", apiKey: apiConfig.openRouter.apikey, dangerouslyAllowBrowser: true, + baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true, defaultHeaders: { "HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. "X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai. }, }); - options = { model: apiConfig.openRouter.model, messages: messages, stream: true, } + options = { model: voidConfig.openRouter.model, messages: messages, stream: true, } } - else if (apiConfig.whichApi === 'openAICompatible') { - openai = new OpenAI({ baseURL: apiConfig.openAICompatible.endpoint, apiKey: apiConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true }); - options = { model: apiConfig.openAICompatible.model, messages: messages, stream: true, }; + else if (voidConfig.default.whichApi === 'openAICompatible') { + openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true }) + options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, } } else { - onError(`Invalid API: ${apiConfig.whichApi}`); - throw new Error(`apiConfig.whichAPI was invalid: ${apiConfig.whichApi}`); + console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`) + throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`) } openai.chat.completions .create(options) - .then(async (response) => { + .then(async response => { abort = () => { - response.controller.abort(); + // response.controller.abort() didAbort = true; - }; - try { - for await (const chunk of response) { - if (didAbort) return; - const newText = chunk.choices[0]?.delta?.content || ''; - fullText += newText; - onText(newText, fullText); + } + // when receive text + for await (const chunk of response) { + if (didAbort) return; + const newText = chunk.choices[0]?.delta?.content || ''; + fullText += newText; + onText(newText, fullText); + } + onFinalMessage(fullText); + }) + // when error/fail - this catches errors of both .create() and .then(for await) + .catch(error => { + if (error instanceof OpenAI.APIError) { + if (error.status === 401) { + onError('Invalid API key.'); } - if (!didAbort) { - onFinalMessage(fullText); - } - } catch (error) { - onError(`Error in stream: ${error}`); - console.error('Error in OpenAI stream:', error); - if (!didAbort) { - onFinalMessage(fullText); + else { + onError(error.message); } } + else { + onError(error); + } }) - .catch((responseError) => { - if (responseError.status === 401) { - onError('Unauthorized: Invalid API key'); - } else if (responseError.status === 400 && responseError.param === 'stream') { - onError(`The model '${model}' does not support streamed responses.`); - } else { - onError(responseError.message); - } - }); return { abort }; }; + // Ollama -const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ - messages, - onText, - onFinalMessage, - onError, - apiConfig -}) => { - const { endpoint, model } = apiConfig.ollama; +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { - if (!endpoint) { - onError('Ollama endpoint not set'); - return { abort: () => {} }; - } - - let didAbort = false; - let fullText = ''; - - const ollama = new Ollama({ host: endpoint }); + let didAbort = false + let fullText = "" + // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either let abort = () => { didAbort = true; }; - ollama - .chat({ - model: model, - messages: messages, - stream: true - }) - .then(async (stream) => { + const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) + + ollama.chat({ + model: voidConfig.ollama.model, + messages: messages, + stream: true, + }) + .then(async stream => { abort = () => { - ollama.abort(); - didAbort = true; - }; - try { - for await (const chunk of stream) { - if (didAbort) return; - const newText = chunk.message.content; - fullText += newText; - onText(newText, fullText); - } - if (!didAbort) { - onFinalMessage(fullText); - } - } catch (error) { - onError(`Error while streaming response: ${error}`); - console.error('Error while streaming response:', error); - if (!didAbort) { - onFinalMessage(fullText); - } + // ollama.abort() + didAbort = true } + // iterate through the stream + for await (const chunk of stream) { + if (didAbort) return; + const newText = chunk.message.content; + fullText += newText; + onText(newText, fullText); + } + onFinalMessage(fullText); + + }) + // when error/fail + .catch(error => { + onError(error) }) - .catch((responseError) => { - if (responseError.error) { - onError(responseError.error.charAt(0).toUpperCase() + responseError.error.slice(1)); - } else { - onError(responseError.message); - } - console.error(responseError); - }); return { abort }; }; + + // Greptile -const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ - messages, - onText, - onFinalMessage, - onError, - apiConfig, -}) => { - const { apikey, githubPAT, repoinfo } = apiConfig.greptile; +// https://docs.greptile.com/api-reference/query +// https://docs.greptile.com/quickstart#sample-response-streamed - if (!apikey) { - return handleMissingApiKey('Greptile', onError); - } - if (!githubPAT) { - onError('GitHub token not set'); - return { abort: () => {} }; - } +const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { - let didAbort = false; - let fullText = ''; + let didAbort = false + let fullText = '' + + // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either + let abort: () => void = () => { didAbort = true } - const controller = new AbortController(); fetch('https://api.greptile.com/v2/query', { method: 'POST', headers: { - Authorization: `Bearer ${apikey}`, - 'X-Github-Token': `${githubPAT}`, - 'Content-Type': `application/json` + "Authorization": `Bearer ${voidConfig.greptile.apikey}`, + "X-Github-Token": `${voidConfig.greptile.githubPAT}`, + "Content-Type": `application/json`, }, body: JSON.stringify({ messages, stream: true, - repositories: [repoinfo] + repositories: [voidConfig.greptile.repoinfo] }), - signal: controller.signal }) - .then((response) => { - if (response.status === 401) { - onError('Unauthorized: Invalid Greptile API key'); - return null; - } else if (response.status !== 200) { - onError(`Error: ${response.status} ${response.statusText}`); - return null; - } - return response.body; + // this is {message}\n{message}\n{message}...\n + .then(async response => { + const text = await response.text() + console.log('got greptile', text) + return JSON.parse(`[${text.trim().split('\n').join(',')}]`) }) - .then(async (body) => { - if (!body || didAbort) return; - const reader = body.getReader(); - const decoder = new TextDecoder('utf-8'); - while (!didAbort) { - const { done, value } = await reader.read(); - if (done || didAbort) break; - const chunk = decoder.decode(value, { stream: true }); - const messages = chunk.trim().split('\n').filter(Boolean); - for (const msg of messages) { - try { - const parsed = JSON.parse(msg); - const { type, message } = parsed; - if (type === 'message' || type === 'sources') { - fullText += message; - onText(message, fullText); - } else if (type === 'status' && !message) { - if (!didAbort) { - onFinalMessage(fullText); - } - } - } catch (e) { - console.error('Error parsing Greptile response:', e); - onError(`Error parsing Greptile response: ${e}`); + // TODO make this actually stream, right now it just sends one message at the end + .then(async responseArr => { + if (didAbort) + return + + for (let response of responseArr) { + + const type: string = response['type'] + const message = response['message'] + + // when receive text + if (type === 'message') { + fullText += message + onText(message, fullText) + } + else if (type === 'sources') { + const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null } + fullText += filepath + onText(filepath, fullText) + } + // type: 'status' with an empty 'message' means last message + else if (type === 'status') { + if (!message) { + onFinalMessage(fullText) } } } + }) - .catch((e) => { - if (didAbort) return; - console.error('Error in Greptile stream:', e); - onError(`Error in Greptile stream: ${e}`); - if (!didAbort) { - onFinalMessage(fullText); - } + .catch(e => { + onError(e) }); - const abort = () => { - controller.abort(); - didAbort = true; - }; + return { abort } - return { abort }; -}; +} -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ - messages, - onText, - onFinalMessage, - onError, - apiConfig, -}) => { - if (!apiConfig) { - onError('API configuration is missing'); - return { abort: () => {} }; - } - switch (apiConfig.whichApi) { + +export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { + if (!voidConfig) return { abort: () => { } } + + switch (voidConfig.default.whichApi) { case 'anthropic': - - return sendClaudeMsg({ - messages, - onText, - onFinalMessage, - onError, - apiConfig, - }); + return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig }); case 'openAI': case 'openRouter': case 'openAICompatible': - return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, apiConfig }); - case 'greptile': - return sendGreptileMsg({ - messages, - onText, - onFinalMessage, - onError, - apiConfig - }); + return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig }); case 'ollama': - - return sendOllamaMsg({ - messages, - onText, - onFinalMessage, - onError, - apiConfig - }); - + return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig }); + case 'greptile': + return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig }); default: - onError(`Error: whichApi was '${apiConfig.whichApi}', which is not recognized!`); - return { abort: () => {} }; + onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) + return { abort: () => { } } } -} \ No newline at end of file +} diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 55c53350..a2724435 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -2,53 +2,12 @@ import * as vscode from 'vscode'; import { DisplayChangesProvider } from './DisplayChangesProvider'; import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './shared_types'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; -import { ApiConfig } from './common/sendLLMMessage'; const readFileContentOfUri = async (uri: vscode.Uri) => { return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8') .replace(/\r\n/g, '\n') // replace windows \r\n with \n } - -const getApiConfig = () => { - const apiConfig: ApiConfig = { - anthropic: { - apikey: vscode.workspace.getConfiguration('void.anthropic').get('apiKey') ?? '', - model: vscode.workspace.getConfiguration('void.anthropic').get('model') ?? '', - maxTokens: vscode.workspace.getConfiguration('void.anthropic').get('maxTokens') ?? '', - }, - openAI: { - apikey: vscode.workspace.getConfiguration('void.openAI').get('apiKey') ?? '', - model: vscode.workspace.getConfiguration('void.openAI').get('model') ?? '', - }, - greptile: { - apikey: vscode.workspace.getConfiguration('void.greptile').get('apiKey') ?? '', - githubPAT: vscode.workspace.getConfiguration('void.greptile').get('githubPAT') ?? '', - repoinfo: { - remote: 'github', - repository: 'TODO', - branch: 'main' - } - }, - ollama: { - endpoint: vscode.workspace.getConfiguration('void.ollama').get('endpoint') ?? '', - model: vscode.workspace.getConfiguration('void.ollama').get('model') ?? '', - }, - openAICompatible: { - endpoint: vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint') ?? '', - model: vscode.workspace.getConfiguration('void.openAICompatible').get('model') ?? '', - apikey: vscode.workspace.getConfiguration('void.openAICompatible').get('apiKey') ?? '', - }, - openRouter: { - model: vscode.workspace.getConfiguration('void.openRouter').get('model') ?? '', - apikey: vscode.workspace.getConfiguration('void.openRouter').get('apiKey') ?? '', - }, - whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? '' - } - return apiConfig -} - - export function activate(context: vscode.ExtensionContext) { // 1. Mount the chat sidebar @@ -95,10 +54,6 @@ export function activate(context: vscode.ExtensionContext) { displayChangesProvider.rejectDiff(params) })); - context.subscriptions.push(vscode.commands.registerCommand('void.openSettings', async () => { - vscode.commands.executeCommand('workbench.action.openSettings', '@ext:void.void'); - })); - // 5. Receive messages from sidebar webviewProvider.webview.then( webview => { @@ -110,15 +65,9 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => { webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar) })) - - // when config changes, send it to the sidebar - vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('void')) { - const apiConfig = getApiConfig() - webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar) - } - }) - + context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => { + webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar) + })); // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) webview.onDidReceiveMessage(async (m: MessageFromSidebar) => { @@ -166,9 +115,13 @@ export function activate(context: vscode.ExtensionContext) { displayChangesProvider.refreshDiffAreas(editor.document.uri) } - else if (m.type === 'getApiConfig') { - const apiConfig = getApiConfig() - webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar) + else if (m.type === 'getPartialVoidConfig') { + const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {} + webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar) + } + else if (m.type === 'persistPartialVoidConfig') { + const partialVoidConfig = m.partialVoidConfig + context.globalState.update('partialVoidConfig', partialVoidConfig) } else if (m.type === 'getAllThreads') { const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {} diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index 0fcbef42..a1e3122e 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { ApiConfig } from './common/sendLLMMessage'; +import { PartialVoidConfig } from './sidebar/contextForConfig'; @@ -42,17 +42,19 @@ type Diff = { type MessageToSidebar = ( | { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor | { type: 'files', files: { filepath: vscode.Uri, content: string }[] } - | { type: 'apiConfig', apiConfig: ApiConfig } + | { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig } | { type: 'allThreads', threads: ChatThreads } | { type: 'startNewThread' } | { type: 'toggleThreadSelector' } + | { type: 'toggleSettings' } ) // sidebar -> editor type MessageFromSidebar = ( | { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar | { type: 'requestFiles', filepaths: vscode.Uri[] } - | { type: 'getApiConfig' } + | { type: 'getPartialVoidConfig' } + | { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig } | { type: 'getAllThreads' } | { type: 'persistThread', thread: ChatThreads[string] } ) diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 9f769dc1..36029f01 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -1,23 +1,38 @@ import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react" -import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage" import { CodeSelection, ChatMessage, MessageToSidebar } from "../shared_types" -import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi" +import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi" import { SidebarThreadSelector } from "./SidebarThreadSelector"; -import { useThreads } from "./threadsContext"; +import { useThreads } from "./contextForThreads"; import { SidebarChat } from "./SidebarChat"; +import { SidebarSettings } from './SidebarSettings'; const Sidebar = () => { - const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false) - const [requestFailed, setRequestFailed] = useState(false) - const [requestFailedReason, setRequestFailedReason] = useState('') + const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat') + + // if they pressed the + to add a new chat + useOnVSCodeMessage('startNewThread', (m) => { + setTab('chat') + }) + + // if they toggled thread selector + useOnVSCodeMessage('toggleThreadSelector', (m) => { + if (tab === 'threadSelector') + setTab('chat') + else + setTab('threadSelector') + }) + + // if they toggled settings + useOnVSCodeMessage('toggleSettings', (m) => { + if (tab === 'settings') + setTab('chat') + else + setTab('settings') + }) - // get Api Config on mount - useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getApiConfig' }) - }, []) // Receive messages from the VSCode extension useEffect(() => { @@ -30,15 +45,24 @@ const Sidebar = () => { }, []) - return <> -
- {isThreadSelectorOpen && ( -
- setIsThreadSelectorOpen(false)} /> -
- )} - + return <> +
+ +
+ setTab('chat')} /> +
+ +
+ +
+ +
+ +
+ + +
diff --git a/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/sidebar/SidebarChat.tsx index 645afb46..87c361d4 100644 --- a/extensions/void/src/sidebar/SidebarChat.tsx +++ b/extensions/void/src/sidebar/SidebarChat.tsx @@ -8,8 +8,9 @@ import { SelectedFiles } from "./components/SelectedFiles"; import { File, ChatMessage, CodeSelection } from "../shared_types"; import * as vscode from 'vscode' import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi"; -import { useThreads } from "./threadsContext"; -import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"; +import { useThreads } from "./contextForThreads"; +import { sendLLMMessage } from "../common/sendLLMMessage"; +import { useVoidConfig } from "./contextForConfig"; @@ -49,7 +50,7 @@ Please edit the file following these instructions: Please edit the selected code following these instructions: `; } - + str += ` \t${instructions} `; @@ -92,7 +93,7 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => { } -export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOpen: (v: boolean | ((v: boolean) => boolean)) => void }) => { +export const SidebarChat = () => { // state of current message @@ -105,10 +106,24 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp const [isLoading, setIsLoading] = useState(false) const abortFnRef = useRef<(() => void) | null>(null) - // higher level state - const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads() - const [apiConfig, setApiConfig] = useState(null) + const [latestError, setLatestError] = useState('') + // higher level state + const { allThreads, currentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads() + const { voidConfig } = useVoidConfig() + + // if they pressed the + to add a new chat + useOnVSCodeMessage('startNewThread', (m) => { + // find a thread with 0 messages and switch to it + for (let threadId in allThreads) { + if (allThreads[threadId].messages.length === 0) { + switchToThread(threadId) + return + } + } + // start a new thread + startNewThread() + }) // if user pressed ctrl+l, add their selection to the sidebar useOnVSCodeMessage('ctrl+l', (m) => { @@ -120,24 +135,6 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp setFiles(files => [...files, filepath]) }) - // when get apiConfig, set - useOnVSCodeMessage('apiConfig', (m) => { - setApiConfig(m.apiConfig) - }) - - // if they pressed the + to add a new chat - useOnVSCodeMessage('startNewThread', (m) => { - setIsThreadSelectorOpen(false) - if (currentThread?.messages.length !== 0) - startNewThread() - - }) - - // if they opened thread selector - useOnVSCodeMessage('toggleThreadSelector', (m) => { - setIsThreadSelectorOpen(v => !v) - }) - const formRef = useRef(null) const onSubmit = async (e: FormEvent) => { @@ -147,34 +144,43 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp setIsLoading(true) setInstructions(''); - formRef.current?.reset(); // reset the form's text + formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens setSelection(null) setFiles([]) + setLatestError('') // request file content from vscode and await response getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) const relevantFiles = await awaitVSCodeResponse('files') // add message to chat history - const content = userInstructionsStr(instructions, relevantFiles.files, selection) + const userContent = userInstructionsStr(instructions, relevantFiles.files, selection) // console.log('prompt:\n', content) - const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } + const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files } addMessageToHistory(newHistoryElt) // send message to LLM let { abort } = sendLLMMessage({ - messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }], + messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userContent }], onText: (newText, fullText) => setMessageStream(fullText), onFinalMessage: (content) => { // add assistant's message to chat history, and clear selection const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } addMessageToHistory(newHistoryElt) - - // clear selection setMessageStream('') setIsLoading(false) }, - apiConfig: apiConfig + onError: (error) => { + // add assistant's message to chat history, and clear selection + let content = messageStream; // just use the current content + const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } + addMessageToHistory(newHistoryElt) + setMessageStream('') + setIsLoading(false) + + setLatestError(error) + }, + voidConfig: voidConfig }) abortFnRef.current = abort @@ -273,6 +279,8 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp
+ + {latestError} } diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx new file mode 100644 index 00000000..450f965e --- /dev/null +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -0,0 +1,96 @@ +import React, { useState } from "react"; +import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig"; + + +const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => { + const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig() + const { enumArr, defaultVal, description } = voidConfigInfo[field][param] + const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item + + const updateState = (newValue: string) => { setConfigParam(field, param, newValue) } + + const resetButton = + + const inputElement = enumArr === undefined ? + // string + ( updateState(e.target.value)} + />) + : + // enum + () + + return
+ + {description} +
+ {inputElement} + {resetButton} +
+
+} + +export const SidebarSettings = () => { + + const { voidConfig, voidConfigInfo } = useVoidConfig() + + const current_field = voidConfig.default['whichApi'] as VoidConfigField + + + return ( +
+ + {/* choose the field */} +
+ +
+ +
+ + {/* render all fields, but hide the ones not visible for fast tab switching */} + {configFields.map(field => { + return
+ {Object.keys(voidConfigInfo[field]).map((param) => ( + + ))} +
+ })} + + {/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */} +
+ {`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `} + {`type 'Open User Settings (JSON)',`} + {` and look for 'void.'. `} +
+
+ ) +} + diff --git a/extensions/void/src/sidebar/SidebarThreadSelector.tsx b/extensions/void/src/sidebar/SidebarThreadSelector.tsx index 287f0420..323d66e5 100644 --- a/extensions/void/src/sidebar/SidebarThreadSelector.tsx +++ b/extensions/void/src/sidebar/SidebarThreadSelector.tsx @@ -1,10 +1,26 @@ import React from "react"; -import { ThreadsProvider, useThreads } from "./threadsContext"; +import { ThreadsProvider, useThreads } from "./contextForThreads"; + + +const truncate = (s: string) => { + let len = s.length + const TRUNC_AFTER = 16 + if (len >= TRUNC_AFTER) + s = s.substring(0, TRUNC_AFTER) + '...' + return s +} + export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => { const { allThreads, currentThread, switchToThread } = useThreads() + + // sorted by most recent to least recent + const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].createdAt > allThreads![threadId2].createdAt ? -1 : 1) + return ( -
+
+ + {/* X button at top right */}
- {/* iterate through all past threads */} - {Object.keys(allThreads ?? {}).map((threadId) => { - const pastThread = (allThreads ?? {})[threadId]; - return ( - - ) - })} + + {/* a list of all the past threads */} +
+ {sortedThreadIds.map((threadId) => { + if (!allThreads) + return <>Error: Threads not found. + const pastThread = allThreads[threadId] + + let btnStringArr = [] + + let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)') + btnStringArr.push(msg1) + + let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '') + if (msg2) + btnStringArr.push(msg2) + + btnStringArr.push(allThreads[threadId].messages.length) + + const btnString = btnStringArr.join(' / ') + + return ( + + ) + })} +
+
) } \ No newline at end of file diff --git a/extensions/void/src/sidebar/contextForConfig.tsx b/extensions/void/src/sidebar/contextForConfig.tsx new file mode 100644 index 00000000..0b0e569a --- /dev/null +++ b/extensions/void/src/sidebar/contextForConfig.tsx @@ -0,0 +1,360 @@ +import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" +import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi" + +const configEnum = (description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => { + return { + description, + defaultVal, + enumArr, + } +} + +const configString = (description: string, defaultVal: string) => { + return { + description, + defaultVal, + enumArr: undefined, + } +} + +// fields you can customize (don't forget 'default' - it isn't included here!) +export const configFields = [ + 'anthropic', + 'openAI', + 'greptile', + 'ollama', + 'openRouter', + 'openAICompatible', + 'azure' +] as const + + + +const voidConfigInfo: Record< + typeof configFields[number] | 'default', { + [prop: string]: { + description: string, + enumArr?: readonly string[] | undefined, + defaultVal: string, + }, + } +> = { + default: { + whichApi: configEnum( + "API Provider.", + 'anthropic', + configFields, + ), + }, + anthropic: { + apikey: configString('Anthropic API key.', ''), + model: configEnum( + "Anthropic model to use.", + 'claude-3-5-sonnet-20240620', + [ + "claude-3-5-sonnet-20240620", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307" + ] as const, + ), + + maxTokens: configEnum( + "Anthropic max number of tokens to output.", + '8192', + [ + "1024", + "2048", + "4096", + "8192" + ] as const, + ), + }, + openAI: { + apikey: configString('OpenAI API key.', ''), + model: configEnum( + 'OpenAI model to use.', + 'gpt-4o', + [ + "o1-preview", + "o1-mini", + "gpt-4o", + "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-turbo-preview", + "gpt-4-0125-preview", + "gpt-4-1106-preview", + "gpt-4", + "gpt-4-0613", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo", + "gpt-3.5-turbo-1106" + ] as const + ), + }, + greptile: { + apikey: configString('Greptile API key.', ''), + githubPAT: configString('Github PAT that Greptile uses to access your repository', ''), + remote: configEnum( + 'Repo location', + 'github', + [ + 'github', + 'gitlab' + ] as const + ), + repository: configString('Repository identifier in "owner/repository" format.', ''), + branch: configString('Name of the branch to use.', 'main'), + }, + ollama: { + endpoint: configString( + 'The Ollama endpoint. Start Ollama by running `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`', + 'http://127.0.0.1:11434' + ), + model: configEnum( + 'Ollama model to use.', + 'llama3.1', + [ + "codegemma", + "codegemma:2b", + "codegemma:7b", + "codellama", + "codellama:7b", + "codellama:13b", + "codellama:34b", + "codellama:70b", + "codellama:code", + "codellama:python", + "command-r", + "command-r:35b", + "command-r-plus", + "command-r-plus:104b", + "deepseek-coder-v2", + "deepseek-coder-v2:16b", + "deepseek-coder-v2:236b", + "falcon2", + "falcon2:11b", + "firefunction-v2", + "firefunction-v2:70b", + "gemma", + "gemma:2b", + "gemma:7b", + "gemma2", + "gemma2:2b", + "gemma2:9b", + "gemma2:27b", + "llama2", + "llama2:7b", + "llama2:13b", + "llama2:70b", + "llama3", + "llama3:8b", + "llama3:70b", + "llama3-chatqa", + "llama3-chatqa:8b", + "llama3-chatqa:70b", + "llama3-gradient", + "llama3-gradient:8b", + "llama3-gradient:70b", + "llama3.1", + "llama3.1:8b", + "llama3.1:70b", + "llama3.1:405b", + "llava", + "llava:7b", + "llava:13b", + "llava:34b", + "llava-llama3", + "llava-llama3:8b", + "llava-phi3", + "llava-phi3:3.8b", + "mistral", + "mistral:7b", + "mistral-large", + "mistral-large:123b", + "mistral-nemo", + "mistral-nemo:12b", + "mixtral", + "mixtral:8x7b", + "mixtral:8x22b", + "moondream", + "moondream:1.8b", + "openhermes", + "openhermes:v2.5", + "phi3", + "phi3:3.8b", + "phi3:14b", + "phi3.5", + "phi3.5:3.8b", + "qwen", + "qwen:7b", + "qwen:14b", + "qwen:32b", + "qwen:72b", + "qwen:110b", + "qwen2", + "qwen2:0.5b", + "qwen2:1.5b", + "qwen2:7b", + "qwen2:72b", + "smollm", + "smollm:135m", + "smollm:360m", + "smollm:1.7b" + ] as const + ), + }, + openRouter: { + model: configString( + 'OpenRouter model to use.', + 'openai/gpt-4o' + ), + apikey: configString('OpenRouter API key.', ''), + }, + openAICompatible: { + endpoint: configString('The endpoint.', 'http://127.0.0.1:11434/v1'), + model: configString('The name of the model to use.', 'gpt-4o'), + apikey: configString('Your API key.', ''), + }, + azure: { + // "void.azure.apiKey": { + // "type": "string", + // "description": "Azure API key." + // }, + // "void.azure.deploymentId": { + // "type": "string", + // "description": "Azure API deployment ID." + // }, + // "void.azure.resourceName": { + // "type": "string", + // "description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`" + // }, + // "void.azure.providerSettings": { + // "type": "object", + // "properties": { + // "baseURL": { + // "type": "string", + // "default": "https://${resourceName}.openai.azure.com/openai/deployments", + // "description": "Azure API base URL." + // }, + // "headers": { + // "type": "object", + // "description": "Custom headers to include in the requests." + // } + // } + // }, + }, +} + + +// this is the type that comes with metadata like desc, default val, etc +type VoidConfigInfo = typeof voidConfigInfo +export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number] + +// this is the type that specifies the user's actual config +export type PartialVoidConfig = { + [K in keyof typeof voidConfigInfo]?: { + [P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal'] + } +} + +export type VoidConfig = { + [K in keyof typeof voidConfigInfo]: { + [P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal'] + } +} + + + +const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => { + const config = {} as PartialVoidConfig + for (let field of [...configFields, 'default'] as const) { + config[field] = {} + for (let prop in voidConfigInfo[field]) { + config[field][prop] = currentConfig[field]?.[prop] || voidConfigInfo[field][prop].defaultVal + } + } + return config as VoidConfig +} + +const defaultVoidConfig: VoidConfig = getVoidConfig({}) + +// const [stateRef, setState] = useInstantState(initVal) +// setState instantly changes the value of stateRef instead of having to wait until the next render +const useInstantState = (initVal: T) => { + const stateRef = useRef(initVal) + const [_, setS] = useState(initVal) + const setState = useCallback((newVal: T) => { + setS(newVal); + stateRef.current = newVal; + }, []) + return [stateRef as React.RefObject, setState] as const // make s.current readonly - setState handles all changes +} + + + +type SetConfigParamType = (field: K, param: keyof VoidConfigInfo[K], newVal: string) => void + +type ConfigValueType = { + voidConfig: VoidConfig, + voidConfigInfo: VoidConfigInfo, + partialVoidConfig: PartialVoidConfig, + setConfigParam: SetConfigParamType +} + + +const ConfigContext = createContext(undefined as unknown as ConfigValueType) + +export function ConfigProvider({ children }: { children: ReactNode }) { + const [partialVoidConfig, setPartialVoidConfig] = useInstantState({}) // the user's selections + const [voidConfig, setVoidConfig] = useState(defaultVoidConfig) + + + // get the config on mount + useEffect(() => { + getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' }) + awaitVSCodeResponse('partialVoidConfig').then((m) => { + setPartialVoidConfig(m.partialVoidConfig) + const newFullConfig = getVoidConfig(m.partialVoidConfig) + setVoidConfig(newFullConfig) + }) + }, [setPartialVoidConfig]) + + // return the provider + return ( { + const newPartialConfig: PartialVoidConfig = { + ...partialVoidConfig.current, + [field]: { + ...partialVoidConfig.current?.[field], + [param]: newVal + } + } + setPartialVoidConfig(newPartialConfig) + const newFullConfig = getVoidConfig(newPartialConfig) + setVoidConfig(newFullConfig) + getVSCodeAPI().postMessage({ type: 'persistPartialVoidConfig', partialVoidConfig: newPartialConfig }) + } + }} + > + {children} + + ) +} + +export function useVoidConfig(): ConfigValueType { + const context = useContext(ConfigContext) + if (context === undefined) { + throw new Error("useVoidConfig missing Provider") + } + return context +} + diff --git a/extensions/void/src/sidebar/threadsContext.tsx b/extensions/void/src/sidebar/contextForThreads.tsx similarity index 86% rename from extensions/void/src/sidebar/threadsContext.tsx rename to extensions/void/src/sidebar/contextForThreads.tsx index 5ade37f4..5e9b5fd9 100644 --- a/extensions/void/src/sidebar/threadsContext.tsx +++ b/extensions/void/src/sidebar/contextForThreads.tsx @@ -3,7 +3,8 @@ import { ChatMessage, ChatThreads } from "../shared_types" import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi" -type ThreadsContextValue = { +// a "thread" means a chat message history +type ConfigForThreadsValueType = { readonly allThreads: ChatThreads | null, readonly currentThread: ChatThreads[string] | null; addMessageToHistory: (message: ChatMessage) => void; @@ -11,7 +12,7 @@ type ThreadsContextValue = { startNewThread: () => void; } -const ThreadsContext = createContext(undefined as unknown as ThreadsContextValue) +const ThreadsContext = createContext(undefined as unknown as ConfigForThreadsValueType) const createNewThread = () => ({ id: new Date().getTime().toString(), @@ -39,7 +40,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { // this loads allThreads in on mount useEffect(() => { - getVSCodeAPI().postMessage({ type: "getAllThreads" }) + getVSCodeAPI().postMessage({ type: 'getAllThreads' }) awaitVSCodeResponse('allThreads') .then(response => { setAllThreads(response.threads) @@ -90,10 +91,10 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { ) } -export function useThreads(): ThreadsContextValue { - const context = useContext(ThreadsContext) +export function useThreads(): ConfigForThreadsValueType { + const context = useContext(ThreadsContext) if (context === undefined) { - throw new Error("useThreads must be used within a ThreadsProvider") + throw new Error("useThreads missing Provider") } return context } diff --git a/extensions/void/src/sidebar/getVscodeApi.ts b/extensions/void/src/sidebar/getVscodeApi.ts index bdc5b2ed..80f29ff0 100644 --- a/extensions/void/src/sidebar/getVscodeApi.ts +++ b/extensions/void/src/sidebar/getVscodeApi.ts @@ -9,20 +9,22 @@ type Command = MessageToSidebar['type'] const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = { "ctrl+l": [], "files": [], - "apiConfig": [], + "partialVoidConfig": [], "startNewThread": [], "allThreads": [], - "toggleThreadSelector": [] + "toggleThreadSelector": [], + "toggleSettings": [], } // messageType -> id -> res const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = { "ctrl+l": {}, "files": {}, - "apiConfig": {}, + "partialVoidConfig": {}, "startNewThread": {}, "allThreads": {}, - "toggleThreadSelector": {} + "toggleThreadSelector": {}, + "toggleSettings": {}, } diff --git a/extensions/void/src/sidebar/index.tsx b/extensions/void/src/sidebar/index.tsx index 7b01c5db..ac226c95 100644 --- a/extensions/void/src/sidebar/index.tsx +++ b/extensions/void/src/sidebar/index.tsx @@ -1,7 +1,8 @@ import * as React from "react" import * as ReactDOM from "react-dom/client" import Sidebar from "./Sidebar" -import { ThreadsProvider } from "./threadsContext" +import { ThreadsProvider } from "./contextForThreads" +import { ConfigProvider } from "./contextForConfig" // mount the sidebar on the id="root" element if (typeof document === "undefined") { @@ -13,7 +14,9 @@ console.log("Void root Element:", rootElement) const extension = ( - + + + ) const root = ReactDOM.createRoot(rootElement) diff --git a/extensions/void/src/sidebar/styles.css b/extensions/void/src/sidebar/styles.css index 6a1d2159..67dcac2a 100644 --- a/extensions/void/src/sidebar/styles.css +++ b/extensions/void/src/sidebar/styles.css @@ -35,5 +35,9 @@ html { } .input { - @apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border; + @apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border focus:outline-vscode-focus-border; +} + +.dropdown { + @apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border; } diff --git a/extensions/void/tailwind.config.js b/extensions/void/tailwind.config.js index 8d7d825e..59df5107 100644 --- a/extensions/void/tailwind.config.js +++ b/extensions/void/tailwind.config.js @@ -18,8 +18,11 @@ module.exports = { "button-hoverBg": "var(--vscode-button-hoverBackground)", "button-secondary-fg": "var(--vscode-button-secondaryForeground)", "button-secondary-bg": "var(--vscode-button-secondaryBackground)", - "button-secondary-hoverBg": - "var(--vscode-button-secondaryHoverBackground)", + "button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)", + "dropdown-bg": "var(--vscode-settings-dropdownBackground)", + "dropdown-foreground": "var(--vscode-settings-dropdownForeground)", + "dropdown-border": "var(--vscode-settings-dropdownBorder)", + "focus-border": "var(--vscode-focusBorder)", }, }, },