Merge branch 'main' into diff-selections

This commit is contained in:
mp 2024-10-14 21:48:34 -07:00
commit ffab60b4b8
24 changed files with 1102 additions and 957 deletions

View file

@ -4,7 +4,7 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a
There are two main ways to contribute: There are two main ways to contribute:
- Suggest New Features ([discord](https://discord.gg/4GAxHVAD)) - Suggest New Features ([discord](https://discord.gg/RSNjgaugJs))
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3)) - Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes). We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
@ -50,7 +50,7 @@ Now that you're set up, feel free to check out our [Issues](https://github.com/v
Beyond the extension, we very occasionally edit the IDE when we need to access more functionality. If you want to work on the full IDE, please follow the steps below, or see VS Code's full [how to contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. Beyond the extension, we very occasionally edit the IDE when we need to access more functionality. If you want to work on the full IDE, please follow the steps below, or see VS Code's full [how to contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`). Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`). Also make sure you have Python on your system.
Make sure you're on the correct NodeJS version as per `.nvmrc`. Make sure you're on the correct NodeJS version as per `.nvmrc`.
@ -124,8 +124,21 @@ Please don't make big refactors without speaking with us first. We'd like to kee
# Submitting a Pull Request # Submitting a Pull Request
When you've made changes and want to submit them, please submit a pull request. Please submit a pull request once you've made a change. Here are a few guidelines:
- A PR should be about one *single* feature change. The fewer items you change, the more likely the PR is to be accepted.
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
- Your title should clearly describe the change you made.
- Add tags to help us stay organized!
- Please don't open a new Issue for your PR. Just submit the PR.
- Avoid refactoring and making feature changes in the same PR.
- Write good code. For example, a common mistake when people edit Void's config is to hard-code a default value like `'claude-3.5'` in 2+ separate places. Please follow best practices or describe your thought process if you had to compromise.
# Relevant files # Relevant files

View file

@ -8,7 +8,7 @@
"**/.DS_Store": true, "**/.DS_Store": true,
"**/Thumbs.db": true, "**/Thumbs.db": true,
"out": false, "out": false,
"**/node_modules": false "**/node_modules": true
}, },
"search.exclude": { "search.exclude": {
"out": true // set this to false to include "out" folder in search results "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 :)! 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

@ -10,6 +10,8 @@
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.27.1", "@anthropic-ai/sdk": "^0.27.1",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"diff": "^7.0.0",
"ollama": "^0.5.9",
"openai": "^4.57.0" "openai": "^4.57.0"
}, },
"devDependencies": { "devDependencies": {
@ -21,20 +23,18 @@
"@types/node": "^22.5.1", "@types/node": "^22.5.1",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/vscode": "1.92.0", "@types/vscode": "1.89.0",
"@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0", "@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10", "@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "2.4.1", "@vscode/test-electron": "2.4.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"diff": "^6.0.0-beta",
"esbuild": "^0.23.1", "esbuild": "^0.23.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.1", "eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0", "globals": "^15.9.0",
"marked": "^14.1.0", "marked": "^14.1.0",
"ollama": "^0.5.8",
"postcss": "^8.4.41", "postcss": "^8.4.41",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -206,397 +206,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.23.1", "version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
@ -1160,9 +769,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/vscode": { "node_modules/@types/vscode": {
"version": "1.92.0", "version": "1.89.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz",
"integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==", "integrity": "sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2545,10 +2154,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/diff": { "node_modules/diff": {
"version": "6.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-6.0.0.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-NbGtgPSw7il+jeajji1H6iKjCk3r/ANQKw3FFUhGV50+MH5MKIMeUmi53piTr7jlkWcq9eS858qbkRzkehwe+w==", "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.3.1" "node": ">=0.3.1"
@ -3470,21 +3078,6 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -4650,8 +4243,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true, "dev": true
"license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
@ -4823,7 +4415,6 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
}, },
@ -5986,7 +5577,6 @@
"version": "0.5.9", "version": "0.5.9",
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz", "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz",
"integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==", "integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"whatwg-fetch": "^3.6.20" "whatwg-fetch": "^3.6.20"
@ -6691,7 +6281,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@ -8195,7 +7784,6 @@
"version": "3.6.20", "version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/whatwg-url": { "node_modules/whatwg-url": {

View file

@ -19,46 +19,237 @@
"void.whichApi": { "void.whichApi": {
"type": "string", "type": "string",
"default": "anthropic", "default": "anthropic",
"description": "Choose an API provider", "description": "Choose an API provider.",
"enum": [ "enum": [
"openAI",
"openAICompatible",
"anthropic", "anthropic",
"openai", "azure",
"greptile", "ollama",
"ollama" "greptile"
] ]
}, },
"void.anthropicApiKey": { "void.anthropic.apiKey": {
"type": "string", "type": "string",
"default": "", "default": "",
"description": "Anthropic API Key" "description": "Anthropic API key."
}, },
"void.anthropicModel": { "void.anthropic.model": {
"type": "string", "type": "string",
"default": "claude-3-5-sonnet-20240620", "default": "claude-3-5-sonnet-20240620",
"description": "Anthropic Model to use", "description": "Anthropic model to use.",
"enum": [ "enum": [
"claude-3-5-sonnet-20240620" "claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307"
] ]
}, },
"void.openAIApiKey": { "void.anthropic.maxTokens": {
"type": "string", "type": "string",
"default": "", "default": "8192",
"description": "OpenAI API Key" "description": "Anthropic max number of tokens to output.",
"enum": [
"1024",
"2048",
"4096",
"8192"
]
}, },
"void.greptileApiKey": { "void.openAI.apiKey": {
"type": "string", "type": "string",
"default": "", "default": "",
"description": "Greptile API Key" "description": "OpenAI API key."
}, },
"void.githubPAT": { "void.openAI.model": {
"type": "string", "type": "string",
"default": "", "default": "gpt-4o",
"description": "Greptile - Github PAT (gives Greptile access to your repo)" "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.ollamaSettings": { "void.greptile.apiKey": {
"type": "string", "type": "string",
"default": "", "default": "",
"description": "Ollama settings (coming soon...)" "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.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."
} }
} }
}, },
@ -80,6 +271,15 @@
"title": "Discard Diff" "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", "command": "void.openSettings",
"title": "Void settings", "title": "Void settings",
"icon": "$(settings-gear)" "icon": "$(settings-gear)"
@ -117,6 +317,16 @@
], ],
"menus": { "menus": {
"view/title": [ "view/title": [
{
"command": "void.startNewThread",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.toggleThreadSelector",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{ {
"command": "void.openSettings", "command": "void.openSettings",
"when": "view == 'void.viewnumberone'", "when": "view == 'void.viewnumberone'",
@ -141,20 +351,18 @@
"@types/node": "^22.5.1", "@types/node": "^22.5.1",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/vscode": "1.92.0", "@types/vscode": "1.89.0",
"@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0", "@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10", "@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "2.4.1", "@vscode/test-electron": "2.4.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"diff": "^6.0.0-beta",
"esbuild": "^0.23.1", "esbuild": "^0.23.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.1", "eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0", "globals": "^15.9.0",
"marked": "^14.1.0", "marked": "^14.1.0",
"ollama": "^0.5.8",
"postcss": "^8.4.41", "postcss": "^8.4.41",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -167,6 +375,8 @@
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.27.1", "@anthropic-ai/sdk": "^0.27.1",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"openai": "^4.57.0" "ollama": "^0.5.9",
"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

@ -14,21 +14,74 @@ function getNonce() {
export class SidebarWebviewProvider implements vscode.WebviewViewProvider { export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewId = 'void.viewnumberone'; public static readonly viewId = 'void.viewnumberone';
public webview: Promise<vscode.Webview> // used to send messages to the webview public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
private readonly _extensionUri: vscode.Uri
private _res: (c: vscode.Webview) => void // used to resolve the webview private _res: (c: vscode.Webview) => void // used to resolve the webview
private readonly _extensionUri: vscode.Uri
private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration
private _webviewDeps: string[] = [];
constructor(context: vscode.ExtensionContext) { constructor(context: vscode.ExtensionContext) {
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later, not sure for what though... was included in webviewProvider code // const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
this._extensionUri = context.extensionUri this._extensionUri = context.extensionUri
let temp_res: typeof this._res | undefined = undefined let temp_res: typeof this._res | undefined = undefined
this.webview = new Promise((res, rej) => { temp_res = res }) 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("sidebar provider: resolver was undefined")
this._res = temp_res 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'];
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);
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));
const nonce = getNonce();
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
webview.html = webviewHTML;
}
// called internally by vscode // called internally by vscode
resolveWebviewView( resolveWebviewView(
webviewView: vscode.WebviewView, webviewView: vscode.WebviewView,
@ -36,43 +89,17 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
token: vscode.CancellationToken, token: vscode.CancellationToken,
) { ) {
const webview = webviewView.webview const webview = webviewView.webview;
webview.options = { webview.options = {
enableScripts: true, enableScripts: true,
localResourceRoots: [this._extensionUri] localResourceRoots: [this._extensionUri]
}; };
// This allows us to use React in vscode this.updateWebviewHTML(webview);
// when you run `npm run build`, we take the React code in the `sidebar` folder
// and compile it into `dist/sidebar/index.js` and `dist/sidebar/styles.css`
// we render that code here
const rootPath = this._extensionUri;
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/index.js'));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/styles.css'));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath));
const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com']
webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}';style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
// resolve webview and _webviewView
this._res(webview); this._res(webview);
this._webviewView = webviewView;
} }
} }

View file

@ -1,16 +1,18 @@
import Anthropic from '@anthropic-ai/sdk'; import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai'; import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
// import ollama from 'ollama'
// always compare these against package.json to make sure every setting in this type can actually be provided by the user
export type ApiConfig = { export type ApiConfig = {
anthropic: { anthropic: {
apikey: string, apikey: string,
model: string, model: string,
maxTokens: string maxTokens: string
}, },
openai: { openAI: {
apikey: string apikey: string,
model: string,
}, },
greptile: { greptile: {
apikey: string, apikey: string,
@ -22,8 +24,14 @@ export type ApiConfig = {
} }
}, },
ollama: { ollama: {
// TODO endpoint: string,
model: string
}, },
openAICompatible: {
endpoint: string,
model: string,
apikey: string
}
whichApi: string whichApi: string
} }
@ -100,31 +108,44 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
// OpenAI // OpenAI and OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let did_abort = false let didAbort = false
let fullText = '' let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => { did_abort = true } let abort: () => void = () => {
didAbort = true;
};
const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true }); let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
openai.chat.completions.create({ if (apiConfig.whichApi === 'openAI') {
model: 'gpt-4o-2024-08-06', openai = new OpenAI({ baseURL: apiConfig.openAICompatible.endpoint, apiKey: apiConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
messages: messages, options = { model: apiConfig.openAI.model, messages: messages, stream: true, }
stream: true, }
}) else if (apiConfig.whichApi === 'openAICompatible') {
openai = new OpenAI({ apiKey: apiConfig.openAI.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(options)
.then(async response => { .then(async response => {
abort = () => { abort = () => {
// response.controller.abort() // this isn't needed now, to keep consistency with claude will leave it commented // response.controller.abort()
did_abort = true; didAbort = true;
} }
// when receive text // when receive text
try { try {
for await (const chunk of response) { for await (const chunk of response) {
if (did_abort) return; if (didAbort) return;
const newText = chunk.choices[0]?.delta?.content || ''; const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText; fullText += newText;
onText(newText, fullText); onText(newText, fullText);
@ -136,8 +157,49 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
console.error('Error in OpenAI stream:', error); console.error('Error in OpenAI stream:', error);
onFinalMessage(fullText); onFinalMessage(fullText);
} }
// when we get the final message on this stream })
onFinalMessage(fullText) return { abort };
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let didAbort = false
let fullText = ""
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort = () => {
didAbort = true;
};
const ollama = new Ollama({ host: apiConfig.ollama.endpoint })
ollama.chat({
model: apiConfig.ollama.model,
messages: messages,
stream: true,
})
.then(async stream => {
abort = () => {
// ollama.abort()
didAbort = true
}
// iterate through the stream
try {
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) {
console.error('Error:', error);
onFinalMessage(fullText);
}
}) })
return { abort }; return { abort };
}; };
@ -150,11 +212,11 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let did_abort = false let didAbort = false
let fullText = '' let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => { did_abort = true } let abort: () => void = () => { didAbort = true }
fetch('https://api.greptile.com/v2/query', { fetch('https://api.greptile.com/v2/query', {
@ -178,7 +240,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
}) })
// TODO make this actually stream, right now it just sends one message at the end // TODO make this actually stream, right now it just sends one message at the end
.then(async responseArr => { .then(async responseArr => {
if (did_abort) if (didAbort)
return return
for (let response of responseArr) { for (let response of responseArr) {
@ -213,74 +275,26 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
return { abort } return { abort }
} }
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
if (!apiConfig) return { abort: () => { } } if (!apiConfig) return { abort: () => { } }
const whichApi = apiConfig.whichApi switch (apiConfig.whichApi) {
case 'anthropic':
if (whichApi === 'anthropic') { return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig });
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) case 'openAI':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig });
case 'greptile':
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig });
default:
console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`);
return { abort: () => { } }
//return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO
} }
else if (whichApi === 'openai') {
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig })
}
else if (whichApi === 'greptile') {
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig })
}
else if (whichApi === 'ollama') {
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
}
else {
console.error(`Error: whichApi was ${whichApi}, which is not recognized!`)
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
}
} }
// Ollama
// const sendOllamaMsg: sendMsgFnType = ({ messages, onText, onFinalMessage }) => {
// let did_abort = false
// let fullText = ''
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// let abort: () => void = () => {
// did_abort = true
// }
// ollama.chat({ model: 'llama3.1', messages: messages, stream: true })
// .then(async response => {
// abort = () => {
// // response.abort() // this isn't needed now, to keep consistency with claude will leave it commented for now
// did_abort = true;
// }
// // when receive text
// try {
// for await (const part of response) {
// if (did_abort) return
// let newText = part.message.content
// fullText += newText
// onText(newText, fullText)
// }
// }
// // when error/fail
// catch (e) {
// onFinalMessage(fullText)
// return
// }
// // when we get the final message on this stream
// onFinalMessage(fullText)
// })
// return { abort };
// };

View file

@ -1,7 +1,6 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { BaseDiffArea, WebviewMessage } from './shared_types';
import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider';
import { DisplayChangesProvider } from './DisplayChangesProvider'; import { DisplayChangesProvider } from './DisplayChangesProvider';
import { BaseDiffArea, ChatThreads, WebviewMessage } from './shared_types';
import { SidebarWebviewProvider } from './SidebarWebviewProvider'; import { SidebarWebviewProvider } from './SidebarWebviewProvider';
import { ApiConfig } from './common/sendLLMMessage'; import { ApiConfig } from './common/sendLLMMessage';
@ -14,14 +13,17 @@ const readFileContentOfUri = async (uri: vscode.Uri) => {
const getApiConfig = () => { const getApiConfig = () => {
const apiConfig: ApiConfig = { const apiConfig: ApiConfig = {
anthropic: { anthropic: {
apikey: vscode.workspace.getConfiguration('void').get('anthropicApiKey') ?? '', apikey: vscode.workspace.getConfiguration('void.anthropic').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void').get('anthropicModel') ?? '', model: vscode.workspace.getConfiguration('void.anthropic').get('model') ?? '',
maxTokens: vscode.workspace.getConfiguration('void').get('anthropicMaxToken') ?? '', 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') ?? '',
}, },
openai: { apikey: vscode.workspace.getConfiguration('void').get('openAIApiKey') ?? '' },
greptile: { greptile: {
apikey: vscode.workspace.getConfiguration('void').get('greptileApiKey') ?? '', apikey: vscode.workspace.getConfiguration('void.greptile').get('apiKey') ?? '',
githubPAT: vscode.workspace.getConfiguration('void').get('githubPAT') ?? '', githubPAT: vscode.workspace.getConfiguration('void.greptile').get('githubPAT') ?? '',
repoinfo: { repoinfo: {
remote: 'github', remote: 'github',
repository: 'TODO', repository: 'TODO',
@ -29,13 +31,20 @@ const getApiConfig = () => {
} }
}, },
ollama: { ollama: {
// apikey: vscode.workspace.getConfiguration('void').get('ollamaSettings') ?? '', 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') ?? '',
}, },
whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? '' whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? ''
} }
return apiConfig return apiConfig
} }
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
// 1. Mount the chat sidebar // 1. Mount the chat sidebar
@ -90,6 +99,14 @@ export function activate(context: vscode.ExtensionContext) {
webviewProvider.webview.then( webviewProvider.webview.then(
webview => { 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 // when config changes, send it to the sidebar
vscode.workspace.onDidChangeConfiguration(e => { vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('void')) { if (e.affectsConfiguration('void')) {
@ -114,8 +131,6 @@ export function activate(context: vscode.ExtensionContext) {
} else if (m.type === 'applyChanges') { } else if (m.type === 'applyChanges') {
console.log('Applying changes')
const editor = vscode.window.activeTextEditor const editor = vscode.window.activeTextEditor
if (!editor) { if (!editor) {
vscode.window.showInformationMessage('No active editor!') vscode.window.showInformationMessage('No active editor!')
@ -148,15 +163,19 @@ export function activate(context: vscode.ExtensionContext) {
} }
else if (m.type === 'getApiConfig') { else if (m.type === 'getApiConfig') {
const apiConfig = getApiConfig() const apiConfig = getApiConfig()
console.log('Api config:', apiConfig)
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) 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 { else {
console.error('unrecognized command', m.type, m) console.error('unrecognized command', m.type, m)
} }
}) })

View file

@ -58,16 +58,55 @@ type WebviewMessage = (
// editor -> sidebar // editor -> sidebar
| { type: 'apiConfig', apiConfig: ApiConfig } | { 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 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: CodeSelection | 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 { export {
BaseDiff, BaseDiffArea, BaseDiff, BaseDiffArea,
Diff, DiffArea,
CodeSelection, CodeSelection,
File, File,
WebviewMessage, WebviewMessage,
Command, Command,
Diff, DiffArea, 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: 'applyChanges', 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 React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage" import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
import { Command, File, CodeSelection, WebviewMessage } from "../shared_types" import { File, CodeSelection, WebviewMessage, ChatMessage } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi" import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
import { marked } from 'marked'; 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 * as vscode from 'vscode'
import { SelectedFiles } from "./components/SelectedFiles";
import { useThreads } from "./threadsContext";
const filesStr = (fullFiles: File[]) => { const filesStr = (fullFiles: File[]) => {
@ -27,7 +30,7 @@ I am currently selecting this code:
\`\`\`${selection.selectionStr}\`\`\` \`\`\`${selection.selectionStr}\`\`\`
`} `}
Please edit the code following these instructions: Please edit the code following these instructions (or, if appropriate, answer my question instead):
${instructions} ${instructions}
If you make a change, rewrite the entire file. If you make a change, rewrite the entire 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 ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role const role = chatMessage.role
@ -81,14 +50,14 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
if (role === 'user') { if (role === 'user') {
chatbubbleContents = <> chatbubbleContents = <>
<IncludedFiles files={chatMessage.files} /> <SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />} {chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{children} {children}
</> </>
} }
else if (role === 'assistant') { 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> </div>
} }
const getBasename = (pathStr: string) => { const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
// "unixify" path const { allThreads, currentThread, switchToThread } = useThreads()
pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with / return (
const parts = pathStr.split('/') // split on / <div className="flex flex-col space-y-1">
return parts[parts.length - 1] <div className="text-right">
} <button className="btn btn-sm" onClick={onClose}>
<svg
type ChatMessage = { xmlns="http://www.w3.org/2000/svg"
role: 'user' fill="none"
content: string, // content sent to the llm viewBox="0 0 24 24"
displayContent: string, // content displayed to user stroke="currentColor"
selection: CodeSelection | null, // the user's selection className="size-4"
files: vscode.Uri[], // the files sent in the message >
} | { <path
role: 'assistant', strokeLinecap="round"
content: string, // content received from LLM strokeLinejoin="round"
displayContent: string // content displayed to user (this is the same as content for now) d="M6 18 18 6M6 6l12 12"
} />
</svg>
</button>
// const [stateRef, setState] = useInstantState(initVal) </div>
// setState instantly changes the value of stateRef instead of having to wait until the next render {/* iterate through all past threads */}
const useInstantState = <T,>(initVal: T) => { {Object.keys(allThreads ?? {}).map((threadId) => {
const stateRef = useRef<T>(initVal) const pastThread = (allThreads ?? {})[threadId];
const [_, setS] = useState<T>(initVal) return (
const setState = useCallback((newVal: T) => { <button
setS(newVal); key={pastThread.id}
stateRef.current = newVal; className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
}, []) onClick={() => switchToThread(pastThread.id)}
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes >
{new Date(pastThread.createdAt).toLocaleString()}
</button>
)
})}
</div>
)
} }
const Sidebar = () => { const Sidebar = () => {
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
// state of current message // state of current message
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
@ -141,9 +117,9 @@ const Sidebar = () => {
const [instructions, setInstructions] = useState('') // the user's instructions const [instructions, setInstructions] = useState('') // the user's instructions
// state of chat // state of chat
const [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([])
const [messageStream, setMessageStream] = useState('') const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null) const abortFnRef = useRef<(() => void) | null>(null)
@ -165,13 +141,12 @@ const Sidebar = () => {
// if user pressed ctrl+l, add their selection to the sidebar // if user pressed ctrl+l, add their selection to the sidebar
if (m.type === 'ctrl+l') { if (m.type === 'ctrl+l') {
setSelection(m.selection) setSelection(m.selection)
const filepath = m.selection.filePath const filepath = m.selection.filePath
// add file if it's not a duplicate // 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]) if (!files.find(f => f.fsPath === filepath.fsPath))
setFiles(files => [...files, filepath])
} }
// when get apiConfig, set // when get apiConfig, set
@ -179,10 +154,22 @@ const Sidebar = () => {
setApiConfig(m.apiConfig) 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); window.addEventListener('message', listener);
return () => { window.removeEventListener('message', listener) } return () => { window.removeEventListener('message', listener) }
}, [files, selection]) }, [files, selection, startNewThread, currentThread])
const formRef = useRef<HTMLFormElement | null>(null) const formRef = useRef<HTMLFormElement | null>(null)
@ -205,17 +192,16 @@ const Sidebar = () => {
const content = userInstructionsStr(instructions, relevantFiles.files, selection) const content = userInstructionsStr(instructions, relevantFiles.files, selection)
// console.log('prompt:\n', content) // console.log('prompt:\n', content)
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) addMessageToHistory(newHistoryElt)
// send message to LLM // send message to LLM
let { abort } = sendLLMMessage({ 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), onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => { onFinalMessage: (content) => {
// add assistant's message to chat history, and clear selection
// add assistant's message to chat history
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) addMessageToHistory(newHistoryElt)
// clear selection // clear selection
setMessageStream('') setMessageStream('')
@ -234,12 +220,12 @@ const Sidebar = () => {
// if messageStream was not empty, add it to the history // if messageStream was not empty, add it to the history
const llmContent = messageStream || '(canceled)' const llmContent = messageStream || '(canceled)'
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent } const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) addMessageToHistory(newHistoryElt)
setMessageStream('') setMessageStream('')
setIsLoading(false) setIsLoading(false)
}, [messageStream]) }, [addMessageToHistory, messageStream])
//Clear code selection //Clear code selection
const clearSelection = () => { const clearSelection = () => {
@ -248,9 +234,14 @@ const Sidebar = () => {
return <> return <>
<div className="flex flex-col h-screen w-full"> <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"> <div className="overflow-y-auto overflow-x-hidden space-y-4">
{/* previous messages */} {/* previous messages */}
{chatMessageHistory.map((message, i) => {currentThread !== null && currentThread.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} /> <ChatBubble key={i} chatMessage={message} />
)} )}
{/* message stream */} {/* message stream */}
@ -260,60 +251,66 @@ const Sidebar = () => {
<div className="shrink-0 py-4"> <div className="shrink-0 py-4">
{/* selection */} {/* selection */}
<div className="text-left"> <div className="text-left">
{/* selected files */}
<FilesSelector files={files} setFiles={setFiles} /> <div className="relative">
{/* selected code */} <div className="input">
{!selection?.selectionStr ? null {/* selection */}
: ( {(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
<div className="relative"> {/* selected files */}
<button <SelectedFiles files={files} setFiles={setFiles} />
onClick={clearSelection} {/* selected code */}
className="absolute top-2 right-2 text-white hover:text-gray-300 z-10" {!!selection?.selectionStr && (
> <BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
X <button
</button> onClick={clearSelection}
<BlockCode text={selection.selectionStr} disableApplyButton={true} /> className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
</div> >
)} 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> </div>
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2 input"
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> </div>

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

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

View file

@ -1,16 +1,20 @@
import * as React from 'react' import * as React from "react"
import * as ReactDOM from 'react-dom/client' import * as ReactDOM from "react-dom/client"
import Sidebar from './Sidebar' import Sidebar from "./Sidebar"
import { ThreadsProvider } from "./threadsContext"
// mount the sidebar on the id="root" element // mount the sidebar on the id="root" element
if (typeof document === 'undefined') { if (typeof document === "undefined") {
console.log('index.tsx error: document was undefined') console.log("index.tsx error: document was undefined")
} }
const rootElement = document.getElementById('root')! const rootElement = document.getElementById("root")!
console.log('root Element', rootElement) console.log("Void root Element:", rootElement)
const extension = (
<ThreadsProvider>
<Sidebar />
</ThreadsProvider>
)
const root = ReactDOM.createRoot(rootElement) 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: "applyChanges", 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; @tailwind utilities;
html { html {
font-size: var(--vscode-font-size); font-size: var(--vscode-font-size);
} }
.btn { .btn {
@apply cursor-pointer transition-colors; @apply cursor-pointer transition-colors;
&.btn-primary { &.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg; @apply bg-vscode-button-bg text-vscode-button-fg;
&:not(:disabled) { &:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg; @apply hover:bg-vscode-button-hoverBg;
} }
} }
&.btn-secondary { &.btn-sm {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg; @apply px-3 py-1 text-sm;
}
&:not(:disabled) { &.btn-secondary {
@apply hover:bg-vscode-button-secondary-hoverBg; @apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
}
}
&:disabled { &:not(:disabled) {
@apply opacity-75 cursor-not-allowed; @apply hover:bg-vscode-button-secondary-hoverBg;
} }
}
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
} }
.input { .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: { extend: {
colors: { colors: {
vscode: { vscode: {
'editor-bg': "var(--vscode-editor-background)", "sidebar-bg": "var(--vscode-sideBar-background)",
'editor-fg': "var(--vscode-editor-foreground)", "editor-bg": "var(--vscode-editor-background)",
'input-bg': "var(--vscode-input-background)", "editor-fg": "var(--vscode-editor-foreground)",
'input-fg': "var(--vscode-input-foreground)", "input-bg": "var(--vscode-input-background)",
'input-border': "var(--vscode-input-border)", "input-fg": "var(--vscode-input-foreground)",
'button-fg': "var(--vscode-button-foreground)", "input-border": "var(--vscode-input-border)",
'button-bg': "var(--vscode-button-background)", "button-fg": "var(--vscode-button-foreground)",
'button-hoverBg': "var(--vscode-button-hoverBackground)", "button-bg": "var(--vscode-button-background)",
'button-secondary-fg': "var(--vscode-button-secondaryForeground)", "button-hoverBg": "var(--vscode-button-hoverBackground)",
'button-secondary-bg': "var(--vscode-button-secondaryBackground)", "button-secondary-fg": "var(--vscode-button-secondaryForeground)",
'button-secondary-hoverBg': "var(--vscode-button-secondaryHoverBackground)", "button-secondary-bg": "var(--vscode-button-secondaryBackground)",
"button-secondary-hoverBg":
"var(--vscode-button-secondaryHoverBackground)",
}, },
}, },
}, },

8
package-lock.json generated
View file

@ -137,6 +137,7 @@
"mocha-multi-reporters": "^1.5.1", "mocha-multi-reporters": "^1.5.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"opn": "^6.0.0", "opn": "^6.0.0",
"original-fs": "^1.2.0",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"p-all": "^1.0.0", "p-all": "^1.0.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
@ -14203,6 +14204,13 @@
"util-deprecate": "~1.0.1" "util-deprecate": "~1.0.1"
} }
}, },
"node_modules/original-fs": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/original-fs/-/original-fs-1.2.0.tgz",
"integrity": "sha512-IGo+qFumpIV65oDchJrqL0BOk9kr82fObnTesNJt8t3YgP6vfqcmRs0ofPzg3D9PKMeBHt7lrg1k/6L+oFdS8g==",
"dev": true,
"license": "Unlicense"
},
"node_modules/os-browserify": { "node_modules/os-browserify": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",

View file

@ -199,6 +199,7 @@
"mocha-multi-reporters": "^1.5.1", "mocha-multi-reporters": "^1.5.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"opn": "^6.0.0", "opn": "^6.0.0",
"original-fs": "^1.2.0",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"p-all": "^1.0.0", "p-all": "^1.0.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",

0
scripts/code.sh Normal file → Executable file
View file

1
void

@ -1 +0,0 @@
Subproject commit 6d513a1e602c8dea3f89f85cc5a04d840a0abfa9