Merge branch 'main' into main

This commit is contained in:
Darion 2024-10-15 11:09:32 -04:00 committed by GitHub
commit b3fd408cf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 939 additions and 598 deletions

View file

@ -149,6 +149,7 @@ We keep track of all the files we've changed with Void so it's easy to rebase:
- CONTRIBUTING.md
- VOID_USEFUL_LINKS.md
- product.json
- package.json
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
- src/vs/workbench/workbench.common.main.ts

View file

@ -8,7 +8,7 @@
"**/.DS_Store": true,
"**/Thumbs.db": true,
"out": false,
"**/node_modules": false
"**/node_modules": true
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results

View file

@ -1 +1,11 @@
Please see the `CONTRIBUTING.md` for information on how to contribute :)!
Here's an overview on how the extension works:
- The extension mounts in `extension.ts`.
- The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a `<script>` tag - see `SidebarWebviewProvider.ts`.
- Communication between the sidebar script and the extension takes place via API. You can search for "postMessage" to see where API calls happen.

View file

@ -75,9 +75,9 @@
}
},
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
"version": "18.19.54",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz",
"integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==",
"version": "18.19.50",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz",
"integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@ -90,13 +90,13 @@
"license": "MIT"
},
"node_modules/@babel/code-frame": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz",
"integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==",
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.25.7",
"@babel/highlight": "^7.24.7",
"picocolors": "^1.0.0"
},
"engines": {
@ -104,9 +104,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz",
"integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==",
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
"dev": true,
"license": "MIT",
"engines": {
@ -114,13 +114,13 @@
}
},
"node_modules/@babel/highlight": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz",
"integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==",
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
"integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.7",
"@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
@ -204,18 +204,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/darwin-arm64": {
"node_modules/@esbuild/win32-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
"cpu": [
"arm64"
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
"win32"
],
"engines": {
"node": ">=18"
@ -312,9 +312,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz",
"integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true,
"license": "MIT",
"engines": {
@ -700,9 +700,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.7.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
"version": "22.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz",
"integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@ -726,9 +726,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz",
"integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==",
"version": "18.3.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -785,17 +785,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
"integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.8.0",
"@typescript-eslint/type-utils": "8.8.0",
"@typescript-eslint/utils": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -819,16 +819,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz",
"integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.8.0",
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/typescript-estree": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4"
},
"engines": {
@ -848,14 +848,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz",
"integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0"
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -866,14 +866,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz",
"integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.8.0",
"@typescript-eslint/utils": "8.8.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -891,9 +891,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz",
"integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true,
"license": "MIT",
"engines": {
@ -905,14 +905,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz",
"integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/visitor-keys": "8.8.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -934,16 +934,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz",
"integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.8.0",
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/typescript-estree": "8.8.0"
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -957,13 +957,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz",
"integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.8.0",
"@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -1477,9 +1477,9 @@
"license": "ISC"
},
"node_modules/browserslist": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
"integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"dev": true,
"funding": [
{
@ -1497,8 +1497,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001663",
"electron-to-chromium": "^1.5.28",
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
@ -1614,9 +1614,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001667",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
"integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
"version": "1.0.30001663",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz",
"integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==",
"dev": true,
"funding": [
{
@ -2192,9 +2192,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.32",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz",
"integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==",
"version": "1.5.28",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz",
"integrity": "sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==",
"dev": true,
"license": "ISC"
},
@ -2452,7 +2452,6 @@
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2506,9 +2505,9 @@
}
},
"node_modules/eslint-plugin-react": {
"version": "7.37.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz",
"integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==",
"version": "7.36.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
"integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3066,21 +3065,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -3203,9 +3187,9 @@
}
},
"node_modules/globals": {
"version": "15.10.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.10.0.tgz",
"integrity": "sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ==",
"version": "15.9.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
"integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
"dev": true,
"license": "MIT",
"engines": {
@ -3407,9 +3391,9 @@
"license": "MIT"
},
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
"integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz",
"integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==",
"dev": true,
"license": "MIT",
"funding": {
@ -4246,8 +4230,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
@ -4419,7 +4402,6 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -5614,9 +5596,9 @@
}
},
"node_modules/openai": {
"version": "4.67.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.67.1.tgz",
"integrity": "sha512-2YbRFy6qaYRJabK2zLMn4txrB2xBy0KP5g/eoqeSPTT31mIJMnkT75toagvfE555IKa2RdrzJrZwdDsUipsAMw==",
"version": "4.63.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.63.0.tgz",
"integrity": "sha512-Y9V4KODbmrOpqiOmCDVnPfMxMqKLOx8Hwcdn/r8mePq4yv7FSXGnxCs8/jZKO7zCB/IVPWihpJXwJNAIOEiZ2g==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
@ -5640,9 +5622,9 @@
}
},
"node_modules/openai/node_modules/@types/node": {
"version": "18.19.54",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz",
"integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==",
"version": "18.19.50",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz",
"integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@ -5826,9 +5808,9 @@
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
@ -6286,7 +6268,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -6404,16 +6385,16 @@
}
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
"integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
"integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"call-bind": "^1.0.6",
"define-properties": "^1.2.1",
"es-errors": "^1.3.0",
"set-function-name": "^2.0.2"
"set-function-name": "^2.0.1"
},
"engines": {
"node": ">= 0.4"
@ -7540,15 +7521,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.0.tgz",
"integrity": "sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.7.0.tgz",
"integrity": "sha512-nEHbEYJyHwsuf7c3V3RS7Saq+1+la3i0ieR3qP0yjqWSzVmh8Drp47uOl9LjbPANac4S7EFSqvcYIKXUUwIfIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.8.0",
"@typescript-eslint/parser": "8.8.0",
"@typescript-eslint/utils": "8.8.0"
"@typescript-eslint/eslint-plugin": "8.7.0",
"@typescript-eslint/parser": "8.7.0",
"@typescript-eslint/utils": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -7679,9 +7660,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"dev": true,
"funding": [
{
@ -7699,8 +7680,8 @@
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"

View file

@ -19,25 +19,26 @@
"void.whichApi": {
"type": "string",
"default": "anthropic",
"description": "Choose an API provider",
"description": "Choose an API provider.",
"enum": [
"openai",
"openAI",
"openRouter",
"openAICompatible",
"anthropic",
"azure",
"greptile",
"ollama",
"openaicompatible"
"greptile"
]
},
"void.anthropic.apiKey": {
"type": "string",
"default": "",
"description": "Anthropic API Key"
"description": "Anthropic API key."
},
"void.anthropic.model": {
"type": "string",
"default": "claude-3-5-sonnet-20240620",
"description": "Anthropic Model to use.",
"description": "Anthropic model to use.",
"enum": [
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
@ -47,7 +48,7 @@
},
"void.anthropic.maxTokens": {
"type": "string",
"default": "1024",
"default": "8192",
"description": "Anthropic max number of tokens to output.",
"enum": [
"1024",
@ -59,12 +60,12 @@
"void.openAI.apiKey": {
"type": "string",
"default": "",
"description": "OpenAI API Key."
"description": "OpenAI API key."
},
"void.openAI.model": {
"type": "string",
"default": "gpt-4o",
"description": "OpenAI model.",
"description": "OpenAI model to use.",
"enum": [
"o1-preview",
"o1-mini",
@ -88,12 +89,55 @@
"void.greptile.apiKey": {
"type": "string",
"default": "",
"description": "Greptile API Key."
"description": "Greptile API key."
},
"void.greptile.githubPAT": {
"type": "string",
"default": "",
"description": "Github PAT given to Greptile to access your repository."
"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",
@ -193,20 +237,30 @@
"smollm:1.7b"
]
},
"void.openaiCompatible.endpoint": {
"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 openai compatible api provider's endpoint. default value is a example of the ollama openai-mode uri"
"description": "The endpoint."
},
"void.openaiCompatible.model": {
"void.openAICompatible.model": {
"type": "string",
"default": "gpt-4o",
"description": "The name of the model to use."
},
"void.openAICompatible.apiKey": {
"type": "string",
"default": "",
"description": "Your provider's model name to use."
},
"void.openaiCompatible.apiKey": {
"type": "string",
"default": "",
"description": "Your provider's API Key."
"description": "Your API key."
}
}
},
@ -227,6 +281,16 @@
"command": "void.discardDiff",
"title": "Discard Diff"
},
{
"command": "void.startNewThread",
"title": "Start a new chat",
"icon": "$(add)"
},
{
"command": "void.toggleThreadSelector",
"title": "View past chats",
"icon": "$(history)"
},
{
"command": "void.openSettings",
"title": "Void settings",
@ -265,6 +329,16 @@
],
"menus": {
"view/title": [
{
"command": "void.startNewThread",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.toggleThreadSelector",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.openSettings",
"when": "view == 'void.viewnumberone'",
@ -315,4 +389,4 @@
"openai": "^4.57.0",
"diff": "^7.0.0"
}
}
}

View file

@ -1,31 +0,0 @@
import * as vscode from 'vscode';
export class CtrlKCodeLensProvider implements vscode.CodeLensProvider {
private codelensesOfDocument: { [documentUri: string]: vscode.CodeLens[] } = {};
// only called by vscode's internals
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUri = document.uri.toString()
return this.codelensesOfDocument[docUri];
}
// only called by us
public addNewCodeLens(document: vscode.TextDocument, selection: vscode.Selection) {
const docUri = document.uri.toString()
if (!this.codelensesOfDocument[docUri])
this.codelensesOfDocument[docUri] = []
// if any other codelens intersects with the selection, don't do it (and have the user now focus that codelens)
for (let lens of this.codelensesOfDocument[docUri]) {
if (lens.range.intersection(selection))
return
}
this.codelensesOfDocument[docUri] = [
...this.codelensesOfDocument[docUri],
new vscode.CodeLens(new vscode.Range(selection.start.line, 0, selection.end.line, Infinity), { title: '', command: '' })];
}
}

View file

@ -20,6 +20,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
private readonly _extensionUri: vscode.Uri
private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration
private _webviewDeps: string[] = [];
constructor(context: vscode.ExtensionContext) {
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
@ -30,8 +31,10 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
if (!temp_res) throw new Error("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 (event.affectsConfiguration('void.ollama.endpoint')) {
if (this._webviewDeps.map(dep => event.affectsConfiguration(dep)).some(v => !!v)) {
if (this._webviewView) {
this.updateWebviewHTML(this._webviewView.webview);
}
@ -39,14 +42,20 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
});
}
// this is updated
private updateWebviewHTML(webview: vscode.Webview) {
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com'];
const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void').get('ollama.endpoint');
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').get('openaiCompatible.endpoint');
if (openaiCompatibleEndpoint)
allowed_urls.push(openaiCompatibleEndpoint);
const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint');
this._webviewDeps.push('void.openAICompatible.endpoint');
if (openAICompatibleEndpoint)
allowed_urls.push(openAICompatibleEndpoint);
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'));

View file

@ -9,9 +9,9 @@ export type ApiConfig = {
model: string;
maxTokens: string;
},
openai: {
apikey: string;
model: string;
openAI: {
apikey: string,
model: string,
},
greptile: {
apikey: string;
@ -26,12 +26,16 @@ export type ApiConfig = {
endpoint: string;
model: string;
},
openaicompatible: {
openAICompatible: {
endpoint: string,
model: string,
apikey: string
},
whichApi: string;
openRouter: {
model: string,
apikey: string
}
whichApi: string
}
type OnText = (newText: string, fullText: string) => void;
@ -136,7 +140,8 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({
return { abort };
};
// OpenAI
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({
messages,
onText,
@ -144,7 +149,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({
onError,
apiConfig,
}) => {
const { apikey, model } = apiConfig.openai;
const { apikey, model } = apiConfig.openAI;
if (!apikey) {
return handleMissingApiKey('OpenAI', onError);
@ -153,21 +158,39 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({
let didAbort = false;
let fullText = '';
const openai = new OpenAI({
apiKey: apikey,
dangerouslyAllowBrowser: true,
});
let abort = () => {
didAbort = true;
};
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
if (apiConfig.whichApi === 'openAI') {
openai = new OpenAI({ apiKey: apiConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: apiConfig.openAI.model, messages: messages, stream: true, }
}
else if (apiConfig.whichApi === 'openRouter') {
openai = new OpenAI({
baseURL: "https://openrouter.ai/api/v1", apiKey: apiConfig.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, }
}
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 {
console.error(`sendOpenAIMsg: invalid whichApi: ${apiConfig.whichApi}`)
throw new Error(`apiConfig.whichAPI was invalid: ${apiConfig.whichApi}`)
}
openai.chat.completions
.create({
model: model,
messages: messages,
stream: true,
})
.create(options)
.then(async (response) => {
abort = () => {
response.controller.abort();
@ -204,56 +227,6 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({
return { abort };
};
// OpenAI Compatible
const sendOpenAICompatibleMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, apiConfig }) => {
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 openai = new OpenAI({ apiKey: apiConfig.openaicompatible.apikey, baseURL: apiConfig.openaicompatible.endpoint, dangerouslyAllowBrowser: true });
openai.chat.completions.create({
model: apiConfig.openaicompatible.model,
messages: messages,
stream: true,
})
.then(async response => {
abort = () => {
response.controller.abort()
didAbort = true;
}
// when receive text
try {
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
catch (error) {
onError(`Error in OpenAI stream:, ${error}`)
console.error('Error in OpenAI stream:', error);
onFinalMessage(fullText);
}
})
.catch((responseError) => {
if (responseError.status === 401) {
onError('Unauthorized: Invalid API key');
} else {
onError(responseError.message);
}
});
return { abort };
};
// Ollama
const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({
messages,
@ -425,6 +398,7 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
switch (apiConfig.whichApi) {
case 'anthropic':
return sendClaudeMsg({
messages,
onText,
@ -432,14 +406,10 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
onError,
apiConfig,
});
case 'openai':
return sendOpenAIMsg({
messages,
onText,
onFinalMessage,
onError,
apiConfig,
});
case 'openAI':
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, apiConfig });
case 'greptile':
return sendGreptileMsg({
messages,
@ -449,6 +419,7 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
apiConfig,
});
case 'ollama':
return sendOllamaMsg({
messages,
onText,
@ -456,16 +427,9 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
onError,
apiConfig,
});
case 'openaicompatible':
return sendOpenAICompatibleMsg({
messages,
onText,
onFinalMessage,
onError,
apiConfig,
});
default:
onError(`Error: whichApi was '${apiConfig.whichApi}', which is not recognized!`);
return { abort: () => {} };
}
};
}

View file

@ -1,6 +1,5 @@
import * as vscode from 'vscode';
import { WebviewMessage } from './shared_types';
import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider';
import { ChatThreads, WebviewMessage } from './shared_types';
import { getDiffedLines } from './getDiffedLines';
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
@ -18,7 +17,7 @@ const getApiConfig = () => {
model: vscode.workspace.getConfiguration('void.anthropic').get('model') ?? '',
maxTokens: vscode.workspace.getConfiguration('void.anthropic').get('maxTokens') ?? '',
},
openai: {
openAI: {
apikey: vscode.workspace.getConfiguration('void.openAI').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.openAI').get('model') ?? '',
},
@ -35,10 +34,14 @@ const getApiConfig = () => {
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') ?? '',
apikey: vscode.workspace.getConfiguration('void.openaiCompatible').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.openaiCompatible').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') ?? ''
}
@ -100,6 +103,14 @@ export function activate(context: vscode.ExtensionContext) {
webviewProvider.webview.then(
webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage)
}))
// when config changes, send it to the sidebar
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('void')) {
@ -135,12 +146,17 @@ export function activate(context: vscode.ExtensionContext) {
await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits)
}
else if (m.type === 'getApiConfig') {
const apiConfig = getApiConfig()
console.log('Api config:', apiConfig)
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
}
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage)
}
else if (m.type === 'persistThread') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
context.workspaceState.update('allThreads', updatedThreads)
}
else {
console.error('unrecognized command', m.type, m)

View file

@ -27,13 +27,52 @@ type WebviewMessage = (
// editor -> sidebar
| { type: 'apiConfig', apiConfig: ApiConfig }
// sidebar -> editor
| { type: 'getAllThreads' }
// editor -> sidebar
| { type: 'allThreads', threads: ChatThreads }
// sidebar -> editor
| { type: 'persistThread', thread: ChatThreads[string] }
// editor -> sidebar
| { type: 'startNewThread' }
// editor -> sidebar
| { type: 'toggleThreadSelector' }
)
type Command = WebviewMessage['type']
type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string;
messages: ChatMessage[];
}
}
type ChatMessage =
| {
role: "user";
content: string; // content sent to the llm
displayContent: string; // content displayed to user
selection: Selection | null; // the user's selection
files: vscode.Uri[]; // the files sent in the message
}
| {
role: "assistant";
content: string; // content received from LLM
displayContent: string; // content displayed to user (this is the same as content for now)
}
export {
Selection,
File,
WebviewMessage,
Command,
ChatThreads,
ChatMessage,
}

View file

@ -1,160 +0,0 @@
import React, { JSX, useState } from 'react';
import { MarkedToken, Token, TokensList } from 'marked';
import { awaitVSCodeResponse, getVSCodeAPI } from './getVscodeApi';
// code block with Apply button at top
export const BlockCode = ({ text, disableApplyButton = false }: { text: string, disableApplyButton?: boolean }) => {
return <div className='py-1'>
{disableApplyButton ? null : <div className='text-sm'>
<button className='btn btn-secondary px-3 py-1 text-sm rounded-t-sm'
onClick={async () => { getVSCodeAPI().postMessage({ type: 'applyCode', code: text }) }}>Apply</button>
</div>}
<div className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${disableApplyButton ? '' : 'rounded-tl-none'}`}>
<pre className='p-3'>
{text}
</pre>
</div>
</div>
}
const Render = ({ token }: { token: Token }) => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.type === "space") {
return <span>{t.raw}</span>;
}
if (t.type === "code") {
return <BlockCode text={t.text} />
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements;
return <HeadingTag>{t.text}</HeadingTag>;
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || 'left' }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td key={cellIndex} style={{ textAlign: t.align[cellIndex] || 'left' }}>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
if (t.type === "hr") {
return <hr />;
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>;
}
if (t.type === "list") {
const ListTag = t.ordered ? 'ol' : 'ul';
return (
<ListTag start={t.start !== '' ? t.start : undefined}
className={`list-inside ${t.ordered ? 'list-decimal' : 'list-disc'}`}
>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
{item.text}
</li>
))}
</ListTag>
);
}
if (t.type === "paragraph") {
return <p>
{t.tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</p>;
}
if (t.type === "html") {
return <pre>{`<html>`}{t.raw}{`</html>`}</pre>;
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>;
}
if (t.type === "def") {
return null; // Definitions are typically not rendered
}
if (t.type === "link") {
return <a href={t.href} title={t.title ?? undefined}>{t.text}</a>;
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />;
}
if (t.type === "strong") {
return <strong>{t.text}</strong>;
}
if (t.type === "em") {
return <em>{t.text}</em>;
}
// inline code
if (t.type === "codespan") {
return <code className='text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono'>{t.text}</code>;
}
if (t.type === "br") {
return <br />;
}
if (t.type === "del") {
return <del>{t.text}</del>;
}
// default
return <div className='bg-orange-50 rounded-sm overflow-hidden'>
<span className='text-xs text-orange-500'>Unknown type:</span>
{t.raw}
</div>;
};
const MarkdownRender = ({ tokens }: { tokens: TokensList }) => {
return (
<>
{tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</>
);
};
export default MarkdownRender;

View file

@ -1,12 +1,15 @@
import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react"
import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage"
import { Command, File, Selection, WebviewMessage } from "../shared_types"
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
import { ChatMessage, File, Selection, WebviewMessage } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
import { marked } from 'marked';
import MarkdownRender, { BlockCode } from "./MarkdownRender";
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import * as vscode from 'vscode'
import { SelectedFiles } from "./components/SelectedFiles";
import { useThreads } from "./threadsContext";
const filesStr = (fullFiles: File[]) => {
@ -35,40 +38,6 @@ If you make a change, rewrite the entire file.
}
const FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => {
return files.length !== 0 && <div className='my-2'>
Include files:
{files.map((filename, i) =>
<div key={i} className='flex'>
{/* X button on a file */}
<button type='button' onClick={() => {
let file_index = files.indexOf(filename)
setFiles([...files.slice(0, file_index), ...files.slice(file_index + 1, Infinity)])
}}>
-{' '}<span className='text-gray-500'>{getBasename(filename.fsPath)}</span>
</button>
</div>
)}
</div>
}
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
return files.length !== 0 && <div className='text-xs my-2'>
{files.map((filename, i) =>
<div key={i} className='flex'>
<button type='button'
className='btn btn-secondary pointer-events-none'
onClick={() => {
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
}}>
-{' '}<span className='text-gray-100'>{getBasename(filename.fsPath)}</span>
</button>
</div>
)}
</div>
}
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role
@ -81,14 +50,14 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
if (role === 'user') {
chatbubbleContents = <>
<IncludedFiles files={chatMessage.files} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />}
<SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{children}
</>
}
else if (role === 'assistant') {
const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer
chatbubbleContents = <MarkdownRender tokens={tokens} /> // sectionsHTML
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
}
@ -99,41 +68,48 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
</div>
}
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with /
const parts = pathStr.split('/') // split on /
return parts[parts.length - 1]
}
type ChatMessage = {
role: 'user'
content: string, // content sent to the llm
displayContent: string, // content displayed to user
selection: Selection | null, // the user's selection
files: vscode.Uri[], // the files sent in the message
} | {
role: 'assistant',
content: string, // content received from LLM
displayContent: string // content displayed to user (this is the same as content for now)
}
// const [stateRef, setState] = useInstantState(initVal)
// setState instantly changes the value of stateRef instead of having to wait until the next render
const useInstantState = <T,>(initVal: T) => {
const stateRef = useRef<T>(initVal)
const [_, setS] = useState<T>(initVal)
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
}, [])
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
const { allThreads, currentThread, switchToThread } = useThreads()
return (
<div className="flex flex-col space-y-1">
<div className="text-right">
<button className="btn btn-sm" onClick={onClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* iterate through all past threads */}
{Object.keys(allThreads ?? {}).map((threadId) => {
const pastThread = (allThreads ?? {})[threadId];
return (
<button
key={pastThread.id}
className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
onClick={() => switchToThread(pastThread.id)}
>
{new Date(pastThread.createdAt).toLocaleString()}
</button>
)
})}
</div>
)
}
const Sidebar = () => {
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
// state of current message
const [selection, setSelection] = useState<Selection | null>(null) // the code the user is selecting
@ -141,13 +117,11 @@ const Sidebar = () => {
const [instructions, setInstructions] = useState('') // the user's instructions
// state of chat
const [chatMessageHistory, setChatMessageHistory] = useState<ChatMessage[]>([])
const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [requestFailed, setRequestFailed] = useState(false)
const [requestFailedReason, setRequestFailedReason] = useState('')
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null)
@ -170,13 +144,12 @@ const Sidebar = () => {
// if user pressed ctrl+l, add their selection to the sidebar
if (m.type === 'ctrl+l') {
setSelection(m.selection)
const filepath = m.selection.filePath
// add file if it's not a duplicate
if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath])
// add current file to the context if it's not already in the files array
if (!files.find(f => f.fsPath === filepath.fsPath))
setFiles(files => [...files, filepath])
}
// when get apiConfig, set
@ -184,10 +157,22 @@ const Sidebar = () => {
setApiConfig(m.apiConfig)
}
// if they pressed the + to add a new chat
else if (m.type === 'startNewThread') {
setIsThreadSelectorOpen(false)
if (currentThread?.messages.length !== 0)
startNewThread()
}
// if they opened thread selector
else if (m.type === 'toggleThreadSelector') {
setIsThreadSelectorOpen(v => !v)
}
}
window.addEventListener('message', listener);
return () => { window.removeEventListener('message', listener) }
}, [files, selection])
}, [files, selection, startNewThread, currentThread])
const formRef = useRef<HTMLFormElement | null>(null)
@ -206,15 +191,6 @@ const Sidebar = () => {
setSelection(null)
setFiles([])
// TODO this is just a hack, turn this into a button instead, and track all histories somewhere
if (instructions === 'clear') {
setChatMessageHistory([])
setMessageStream('')
setIsLoading(false)
return
}
// request file content from vscode and await response
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files')
@ -223,16 +199,18 @@ const Sidebar = () => {
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
// console.log('prompt:\n', content)
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
addMessageToHistory(newHistoryElt)
// send message to LLM
let { abort } = sendLLMMessage({
messages: [...chatMessageHistory.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 }],
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, }
setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
addMessageToHistory(newHistoryElt)
// clear selection
setMessageStream('')
setIsLoading(false)
},
@ -250,12 +228,12 @@ const Sidebar = () => {
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(canceled)'
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
}, [messageStream])
}, [addMessageToHistory, messageStream])
//Clear code selection
const clearSelection = () => {
@ -264,9 +242,14 @@ const Sidebar = () => {
return <>
<div className="flex flex-col h-screen w-full">
{isThreadSelectorOpen && (
<div className="mb-2 max-h-[30vh] overflow-y-auto">
<ThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
</div>
)}
<div className="overflow-y-auto overflow-x-hidden space-y-4">
{/* previous messages */}
{chatMessageHistory.map((message, i) =>
{currentThread !== null && currentThread.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
@ -276,21 +259,65 @@ const Sidebar = () => {
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
{/* selected files */}
<FilesSelector files={files} setFiles={setFiles} />
{/* selected code */}
{!selection?.selectionStr ? null
: (
<div className="relative">
<button
onClick={clearSelection}
className="absolute top-2 right-2 text-white hover:text-gray-300 z-10"
>
X
</button>
<BlockCode text={selection.selectionStr} disableApplyButton={true} />
</div>
)}
<div className="relative">
<div className="input">
{/* selection */}
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
{/* selected files */}
<SelectedFiles files={files} setFiles={setFiles} />
{/* selected code */}
{!!selection?.selectionStr && (
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
<button
onClick={clearSelection}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>
Remove
</button>
)} />
)}
</div>}
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2"
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
onSubmit={(e) => {
console.log('submit!')
e.preventDefault();
onSubmit(e)
}}>
{/* input */}
<textarea
onChange={(e) => { setInstructions(e.target.value) }}
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
placeholder="Ctrl+L to select"
rows={1}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
/>
{/* submit button */}
{isLoading ?
<button
onClick={onStop}
className="btn btn-primary rounded-r-lg max-h-10 p-2"
type='button'
>Stop</button>
: <button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={!instructions}
type='submit'
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
}
</form>
</div>
</div>
</div>
{/* error message */}
{requestFailed && (

View file

@ -0,0 +1,48 @@
import React from "react"
import * as vscode from "vscode"
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
const parts = pathStr.split("/") // split on /
return parts[parts.length - 1]
}
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
return (
files.length !== 0 && (
<div className="flex flex-wrap -mx-1 -mb-1">
{files.map((filename, i) => (
<button
key={filename.path}
disabled={!setFiles}
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
type="button"
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
>
<span>{getBasename(filename.fsPath)}</span>
{/* X button */}
{!!setFiles && <span className="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</span>}
</button>
))}
</div>
)
)
}

View file

@ -10,6 +10,11 @@ const awaiting: { [c in Command]: ((res: any) => void)[] } = {
"files": [],
"apiConfig": [],
"getApiConfig": [],
"startNewThread": [],
"getAllThreads": [],
"allThreads": [],
"persistThread": [],
"toggleThreadSelector": []
}
// use this function to await responses

View file

@ -1,16 +1,20 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import Sidebar from './Sidebar'
import * as React from "react"
import * as ReactDOM from "react-dom/client"
import Sidebar from "./Sidebar"
import { ThreadsProvider } from "./threadsContext"
// mount the sidebar on the id="root" element
if (typeof document === 'undefined') {
console.log('index.tsx error: document was undefined')
if (typeof document === "undefined") {
console.log("index.tsx error: document was undefined")
}
const rootElement = document.getElementById('root')!
console.log('root Element', rootElement)
const rootElement = document.getElementById("root")!
console.log("Void root Element:", rootElement)
const extension = (
<ThreadsProvider>
<Sidebar />
</ThreadsProvider>
)
const root = ReactDOM.createRoot(rootElement)
root.render(<Sidebar />)
root.render(extension)

View file

@ -0,0 +1,80 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi"
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000
// code block with toolbar (Apply, Copy, etc) at top
const BlockCode = ({
text,
toolbar,
hideToolbar = false,
className,
}: {
text: string
toolbar?: ReactNode
hideToolbar?: boolean
className?: string
}) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
const defaultToolbar = (
<>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyCode", code: text })
}}
>
Apply
</button>
</>
)
return (
<div className="relative group">
{!hideToolbar && (
<div className="absolute top-0 right-0 invisible group-hover:visible">
<div className="flex space-x-2 p-2">{toolbar || defaultToolbar}</div>
</div>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
>
<pre className="p-2">{text}</pre>
</div>
</div>
)
}
export default BlockCode

View file

@ -0,0 +1,168 @@
import React, { JSX } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked"
import BlockCode from "./BlockCode"
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.type === "space") {
return <span>{t.raw}</span>
}
if (t.type === "code") {
return <BlockCode text={t.text} />
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
return <HeadingTag>{t.text}</HeadingTag>
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
if (t.type === "hr") {
return <hr />
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
return (
<ListTag
start={t.start ? t.start : undefined}
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
<MarkdownRender string={item.text} nested={true} />
</li>
))}
</ListTag>
)
}
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} />
))}
</>
if (nested)
return contents
return <p>{contents}</p>
}
// don't actually render <html> tags, just render strings of them
if (t.type === "html") {
return (
<pre>
{`<html>`}
{t.raw}
{`</html>`}
</pre>
)
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>
}
if (t.type === "def") {
return <></> // Definitions are typically not rendered
}
if (t.type === "link") {
return (
<a href={t.href} title={t.title ?? undefined}>
{t.text}
</a>
)
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
}
if (t.type === "strong") {
return <strong>{t.text}</strong>
}
if (t.type === "em") {
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
{t.text}
</code>
)
}
if (t.type === "br") {
return <br />
}
// strikethrough
if (t.type === "del") {
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden">
<span className="text-xs text-orange-500">Unknown type:</span>
{t.raw}
</div>
)
}
const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} />
))}
</>
)
}
export default MarkdownRender

View file

@ -3,33 +3,37 @@
@tailwind utilities;
html {
font-size: var(--vscode-font-size);
font-size: var(--vscode-font-size);
}
.btn {
@apply cursor-pointer transition-colors;
@apply cursor-pointer transition-colors;
&.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg;
&.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg;
&:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg;
}
}
&:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg;
}
}
&.btn-secondary {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
&.btn-sm {
@apply px-3 py-1 text-sm;
}
&:not(:disabled) {
@apply hover:bg-vscode-button-secondary-hoverBg;
}
}
&.btn-secondary {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
&:not(:disabled) {
@apply hover:bg-vscode-button-secondary-hoverBg;
}
}
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
}
.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;
}

View file

@ -0,0 +1,100 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { ChatMessage, ChatThreads } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
type ThreadsContextValue = {
readonly allThreads: ChatThreads | null,
readonly currentThread: ChatThreads[string] | null;
addMessageToHistory: (message: ChatMessage) => void;
switchToThread: (threadId: string) => void;
startNewThread: () => void;
}
const ThreadsContext = createContext<ThreadsContextValue>(undefined as unknown as ThreadsContextValue)
const createNewThread = () => ({
id: new Date().getTime().toString(),
createdAt: new Date().toISOString(),
messages: [],
})
// const [stateRef, setState] = useInstantState(initVal)
// setState instantly changes the value of stateRef instead of having to wait until the next render
const useInstantState = <T,>(initVal: T) => {
const stateRef = useRef<T>(initVal)
const [_, setS] = useState<T>(initVal)
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
}, [])
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
}
export function ThreadsProvider({ children }: { children: ReactNode }) {
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
// this loads allThreads in on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: "getAllThreads" })
awaitVSCodeResponse('allThreads')
.then(response => {
setAllThreads(response.threads)
})
}, [setAllThreads])
return (
<ThreadsContext.Provider
value={{
allThreads: allThreads.current,
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
addMessageToHistory: (message: ChatMessage) => {
let currentThread: ChatThreads[string]
if (!(currentThreadId.current === null || allThreads.current === null)) {
currentThread = allThreads.current[currentThreadId.current]
}
else {
currentThread = createNewThread()
setCurrentThreadId(currentThread.id)
}
setAllThreads({
...allThreads.current,
[currentThread.id]: {
...currentThread,
messages: [...currentThread.messages, message],
}
})
getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
},
switchToThread: (threadId: string) => {
setCurrentThreadId(threadId);
},
startNewThread: () => {
const newThread = createNewThread()
setAllThreads({
...allThreads.current,
[newThread.id]: newThread
})
setCurrentThreadId(newThread.id)
},
}}
>
{children}
</ThreadsContext.Provider>
)
}
export function useThreads(): ThreadsContextValue {
const context = useContext<ThreadsContextValue>(ThreadsContext)
if (context === undefined) {
throw new Error("useThreads must be used within a ThreadsProvider")
}
return context
}

View file

@ -7,17 +7,19 @@ module.exports = {
extend: {
colors: {
vscode: {
'editor-bg': "var(--vscode-editor-background)",
'editor-fg': "var(--vscode-editor-foreground)",
'input-bg': "var(--vscode-input-background)",
'input-fg': "var(--vscode-input-foreground)",
'input-border': "var(--vscode-input-border)",
'button-fg': "var(--vscode-button-foreground)",
'button-bg': "var(--vscode-button-background)",
'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)",
"sidebar-bg": "var(--vscode-sideBar-background)",
"editor-bg": "var(--vscode-editor-background)",
"editor-fg": "var(--vscode-editor-foreground)",
"input-bg": "var(--vscode-input-background)",
"input-fg": "var(--vscode-input-foreground)",
"input-border": "var(--vscode-input-border)",
"button-fg": "var(--vscode-button-foreground)",
"button-bg": "var(--vscode-button-background)",
"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)",
},
},
},