Compare commits

..

38 Commits

Author SHA1 Message Date
Andrew Pareles 7311f3eaf7 + 2025-03-03 20:55:04 -08:00
Andrew Pareles fcbad101a4 tool use progress (partial) 2025-03-02 20:31:02 -08:00
Andrew Pareles b6ea05948f fix tw 2025-03-01 19:22:40 -08:00
Andrew Pareles 495dc45c10 downgrade tailwind (v3) 2025-03-01 19:21:33 -08:00
Andrew Pareles e4cf30ef07 fix tailwind version issue 2025-03-01 19:18:51 -08:00
Andrew Pareles 5655280d45 Merge remote-tracking branch 'origin/main' into model-selection 2025-03-01 19:06:40 -08:00
Andrew Pareles c793db8aeb rename 2025-03-01 19:06:30 -08:00
Andrew Pareles f711c68798 Merge pull request #289 from bawsdeep/main
Needed rework of my script to generate the Appimage
2025-03-01 18:50:14 -08:00
Andrew Pareles dbc14a0713 Merge pull request #303 from voideditor/model-selection
Add reasoning parsing, marketplace URL, chat selection state, misc UI
2025-03-01 18:35:58 -08:00
Andrew Pareles 417259123b Merge remote-tracking branch 'origin/main' into model-selection 2025-03-01 18:34:23 -08:00
Andrew Pareles b82d668437 Merge pull request #301 from voideditor/merge-vscode-2
Rebase vscode  onto void (microsoft/vscode 0480008e3221e6743799332fc0018a98e8e90457 -> voideditor/void 1e09852b3c)
2025-03-01 17:50:18 -08:00
Mathew Pareles 93d40be1ac Merge remote-tracking branch 'origin/main' into model-selection 2025-03-01 17:43:18 -08:00
Mathew Pareles cfc39b83bb styles 2025-03-01 17:42:17 -08:00
Mathew Pareles 9c76a35837 outline 2025-03-01 17:42:17 -08:00
Mathew Pareles 4a00bd3e00 Cmd+L selections open better ux 2025-03-01 17:42:17 -08:00
Mathew Pareles 6d6e2fa974 fix annoying refactor 2025-03-01 17:42:17 -08:00
Mathew Pareles 971ce88df2 style 2025-03-01 17:40:29 -08:00
Andrew Pareles aa835d468b misc 2025-02-27 20:13:50 -08:00
Mathew Pareles 661eba3ae9 fix selection state bug 2025-02-27 17:08:49 -08:00
Mathew Pareles de17b3add1 styles 2025-02-27 16:27:34 -08:00
Mathew Pareles 6fa3901ab0 small style 2025-02-27 16:22:13 -08:00
Andrew Pareles d14a2f1fff marketplace URL 2025-02-26 02:22:58 -08:00
Andrew Pareles c1f3b92c51 fix manual reasoning parsing 2025-02-26 02:14:00 -08:00
Andrew Pareles 0de63a088d reasoning UI! 2025-02-26 02:01:04 -08:00
Andrew Pareles 0de1c20551 progress 2025-02-26 02:01:03 -08:00
Mathew Pareles a769eb4db0 Merge branch 'recovered-branch' into model-selection 2025-02-25 19:09:28 -08:00
Mathew Pareles 3e7e54b88f Merge branch 'model-selection' into HEAD 2025-02-25 18:48:38 -08:00
Mathew Pareles 1b43799371 prepare merg 2025-02-25 18:48:17 -08:00
Mathew Pareles 38370c751e + 2025-02-24 05:59:56 -08:00
Mathew Pareles 764b1a2ccf improve input box 2025-02-24 05:47:58 -08:00
Mathew Pareles 1dffbfb061 style 2025-02-24 04:09:09 -08:00
Mathew Pareles df6de2d7fc styles for tool use 2025-02-24 03:51:59 -08:00
Mathew Pareles 4ebfa91a83 fix URI staleness bug in local storage 2025-02-24 00:46:03 -08:00
bawsdeep 08581776ae forgot some files in the final clean up 2025-02-17 14:01:13 +01:00
bawsdeep af6f9fc570 some last changes 2025-02-17 13:28:38 +01:00
bawsdeep ecdef730d5 ai thought were building any application for VOID LINUX 2025-02-17 11:32:01 +01:00
bawsdeep 0f4120c1a0 edit readme 2025-02-17 11:28:44 +01:00
bawsdeep c8db4529b2 new script to create appimage 2025-02-17 11:24:07 +01:00
29 changed files with 1597 additions and 739 deletions
+263 -20
View File
@@ -174,7 +174,7 @@
"source-map": "0.6.1",
"source-map-support": "^0.3.2",
"style-loader": "^3.3.2",
"tailwindcss": "^4.0.9",
"tailwindcss": "^3.4.17",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsec": "0.2.7",
@@ -193,6 +193,19 @@
"windows-foreground-love": "0.5.0"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -6289,6 +6302,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -6370,16 +6393,11 @@
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -6392,6 +6410,9 @@
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
@@ -7721,6 +7742,13 @@
"dev": true,
"optional": true
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/diff": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
@@ -7742,6 +7770,13 @@
"node": ">=8"
}
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -13303,6 +13338,16 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
}
},
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@@ -13891,9 +13936,9 @@
}
},
"node_modules/lilconfig": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -15707,6 +15752,16 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -16894,6 +16949,44 @@
"postcss": "^8.4.31"
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-js": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
@@ -17854,6 +17947,26 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
}
},
"node_modules/read-cache/node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/read-pkg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@@ -18252,18 +18365,22 @@
"dev": true
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.9.0",
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -20449,12 +20566,125 @@
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.9.tgz",
"integrity": "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==",
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.6",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
"sucrase": "^3.35.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss/node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/tailwindcss/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/tailwindcss/node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"postcss": {
"optional": true
},
"ts-node": {
"optional": true
}
}
},
"node_modules/tailwindcss/node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
},
"node_modules/tapable": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
@@ -22730,6 +22960,19 @@
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
"dev": true
},
"node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+1 -1
View File
@@ -235,7 +235,7 @@
"source-map": "0.6.1",
"source-map-support": "^0.3.2",
"style-loader": "^3.3.2",
"tailwindcss": "^4.0.9",
"tailwindcss": "^3.4.17",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsec": "0.2.7",
+2 -2
View File
@@ -31,8 +31,8 @@
"nodejsRepository": "https://nodejs.org",
"urlProtocol": "void",
"extensionsGallery": {
"serviceUrl": "https://open-vsx.org/vscode/gallery",
"itemUrl": "https://open-vsx.org/vscode/item"
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
"itemUrl": "https://marketplace.visualstudio.com/items"
},
"builtInExtensions": []
}
+135
View File
@@ -0,0 +1,135 @@
#!/bin/bash
# Exit on error
set -e
# Check platform
platform=$(uname)
if [[ "$platform" == "Darwin" ]]; then
echo "Running on macOS. Note that the AppImage created will only work on Linux systems."
if ! command -v docker &> /dev/null; then
echo "Docker Desktop for Mac is not installed. Please install it from https://www.docker.com/products/docker-desktop"
exit 1
fi
elif [[ "$platform" == "Linux" ]]; then
echo "Running on Linux. Proceeding with AppImage creation..."
else
echo "This script is intended to run on macOS or Linux. Current platform: $platform"
exit 1
fi
# Enable BuildKit
export DOCKER_BUILDKIT=1
BUILD_IMAGE_NAME="void-appimage-builder"
# Check if Docker is running
if ! docker info >/dev/null 2>&1; then
echo "Docker is not running. Please start Docker first."
exit 1
fi
# Check and install Buildx if needed
if ! docker buildx version >/dev/null 2>&1; then
echo "Installing Docker Buildx..."
mkdir -p ~/.docker/cli-plugins/
curl -SL https://github.com/docker/buildx/releases/download/v0.13.1/buildx-v0.13.1.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod +x ~/.docker/cli-plugins/docker-buildx
fi
# Download appimagetool if not present
if [ ! -f "appimagetool" ]; then
echo "Downloading appimagetool..."
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool
fi
# Delete any existing AppImage to avoid bloating the build
rm -f Void-x86_64.AppImage
# Create build Dockerfile
echo "Creating build Dockerfile..."
cat > Dockerfile.build << 'EOF'
# syntax=docker/dockerfile:1
FROM ubuntu:20.04
# Install required dependencies
RUN apt-get update && apt-get install -y \
libfuse2 \
libglib2.0-0 \
libgtk-3-0 \
libx11-xcb1 \
libxss1 \
libxtst6 \
libnss3 \
libasound2 \
libdrm2 \
libgbm1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
EOF
# Create .dockerignore file
echo "Creating .dockerignore file..."
cat > .dockerignore << EOF
Dockerfile.build
.dockerignore
.git
.gitignore
.DS_Store
*~
*.swp
*.swo
*.tmp
*.bak
*.log
*.err
node_modules/
venv/
*.egg-info/
*.tox/
dist/
EOF
# Build Docker image without cache
echo "Building Docker image (no cache)..."
docker build --no-cache -t "$BUILD_IMAGE_NAME" -f Dockerfile.build .
# Create AppImage using local appimagetool
echo "Creating AppImage..."
docker run --rm --privileged -v "$(pwd):/app" "$BUILD_IMAGE_NAME" bash -c '
cd /app && \
rm -rf VoidApp.AppDir && \
mkdir -p VoidApp.AppDir/usr/bin VoidApp.AppDir/usr/lib VoidApp.AppDir/usr/share/applications && \
find . -maxdepth 1 ! -name VoidApp.AppDir ! -name "." ! -name ".." -exec cp -r {} VoidApp.AppDir/usr/bin/ \; && \
cp void.png VoidApp.AppDir/ && \
echo "[Desktop Entry]" > VoidApp.AppDir/void.desktop && \
echo "Name=Void" >> VoidApp.AppDir/void.desktop && \
echo "Exec=void" >> VoidApp.AppDir/void.desktop && \
echo "Icon=void" >> VoidApp.AppDir/void.desktop && \
echo "Type=Application" >> VoidApp.AppDir/void.desktop && \
echo "Categories=Utility;" >> VoidApp.AppDir/void.desktop && \
echo "Comment=Void Linux Application" >> VoidApp.AppDir/void.desktop && \
chmod +x VoidApp.AppDir/void.desktop && \
cp VoidApp.AppDir/void.desktop VoidApp.AppDir/usr/share/applications/ && \
echo "#!/bin/bash" > VoidApp.AppDir/AppRun && \
echo "HERE=\$(dirname \"\$(readlink -f \"\${0}\")\")" >> VoidApp.AppDir/AppRun && \
echo "export PATH=\${HERE}/usr/bin:\${PATH}" >> VoidApp.AppDir/AppRun && \
echo "export LD_LIBRARY_PATH=\${HERE}/usr/lib:\${LD_LIBRARY_PATH}" >> VoidApp.AppDir/AppRun && \
echo "exec \${HERE}/usr/bin/void --no-sandbox \"\$@\"" >> VoidApp.AppDir/AppRun && \
chmod +x VoidApp.AppDir/AppRun && \
chmod -R 755 VoidApp.AppDir && \
# Strip unneeded symbols from the binary to reduce size
strip --strip-unneeded VoidApp.AppDir/usr/bin/void
ls -la VoidApp.AppDir/ && \
ARCH=x86_64 ./appimagetool -n VoidApp.AppDir Void-x86_64.AppImage
'
# Clean up
rm -rf VoidApp.AppDir .dockerignore appimagetool
echo "AppImage creation complete! Your AppImage is: Void-x86_64.AppImage"
+114
View File
@@ -0,0 +1,114 @@
# Void AppImage Creation Script
This script automates the process of creating an AppImage for the Void Editor using Docker. It works on macOS and Linux platforms.
## Requirements
* **Docker:** The script relies on Docker to build the AppImage inside a container.
* **macOS or Linux:** The script is designed for these platforms. On macOS, it generates a Linux-compatible AppImage.
* **Internet Connection:** Required for downloading necessary tools (like `docker-buildx` and `appimagetool` inside the Docker container).
## Prerequisites
1. **Install Docker:**
* **macOS:** Download and install Docker Desktop from [docker.com](docker.com).
* **Ubuntu:**
```bash
sudo apt install docker.io
```
* **Arch Linux:**
```bash
sudo pacman -S docker
```
* **Fedora:**
```bash
sudo dnf install docker
```
2. **Set Docker User Group:**
Docker requires users to be part of the `docker` group to run Docker commands without `sudo`.
```bash
sudo usermod -aG docker $USER
```
After running this command, log out and log back in for the group changes to take effect.
3. **Enable and Start Docker:**
```bash
sudo systemctl enable docker
sudo systemctl start docker
```
## Ubuntu Dependencies (Installed via Docker)
These dependencies are installed within the Docker container (Ubuntu 20.04 base). You generally don't need to install them manually:
* `libfuse2`
* `libglib2.0-0`
* `libgtk-3-0`
* `libx11-xcb1`
* `libxss1`
* `libxtst6`
* `libnss3`
* `libasound2`
* `libdrm2`
* `libgbm1`
## Usage Instructions
1. **Clone or Download the Script:**
Save the script to your system as `create_appimage.sh`.
2. **Make the Script Executable:**
```bash
chmod +x create_appimage.sh
```
3. **Copy Required Files:**
Copy the following files to the directory where the app binary is being bundled (created during the build process):
* `create_appimage.sh`
* `void.desktop`
* `void.png`
4. **Run the Script:**
```bash
./create_appimage.sh
```
5. **Result:**
After the script completes, it will generate an AppImage named `Void-x86_64.AppImage` (or similar, depending on your architecture) in the current directory.
## Script Overview
* **Platform Check:** Checks for macOS or Linux. Exits if unsupported.
* **Docker Checks:** Ensures Docker is installed and running.
* **Buildx Installation:** Installs `docker buildx` if missing.
* **`appimagetool` Download:** Downloads `appimagetool` inside the Docker container.
* **Dockerfile Creation:** Creates a temporary `Dockerfile.build` for the Ubuntu-based environment.
* **Docker Image Build:** Builds a Docker image and runs the build process.
* **AppImage Creation:**
* Creates the `VoidApp.AppDir` structure.
* Copies binaries, resources, and the `.desktop` entry.
* Copies `void.desktop` and `void.png`.
* Strips unnecessary symbols from the binary.
* Runs `appimagetool` to generate the AppImage.
* **Cleanup:** Removes the temporary `Dockerfile.build`.
## Troubleshooting
* **Docker Not Running:** Ensure Docker is installed and running.
* **Permission Issues:** Try running the script with `sudo` or check Docker permissions.
* **Outdated Dependencies:** Ensure you have the minimum required versions.
## License
This script is provided "as is". It is free to use, modify, and distribute, but comes with no warranty.
+7
View File
@@ -0,0 +1,7 @@
[Desktop Entry]
Name=void
Exec=void
Icon=void
Type=Application
Categories=Utility;
Comment=Void Linux Application
Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

@@ -3,185 +3,106 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
// import { URI } from '../../../../base/common/uri.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// import { IToolService, ToolService } from '../common/toolsService.js';
// 1. search(ai)
// - tool use to find all possible changes
// - if search only: is this file related to the search?
// - if search + replace: should I modify this file?
// 2. replace(ai)
// - what changes to make?
// 3. postprocess errors
// -fastapply changes simultaneously
// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error)
// private async _searchUsingAI({ searchClause }: { searchClause: string }) {
export type ChatMessageLocation = {
threadId: string;
messageIdx: number;
}
// // const relevantURIs: URI[] = []
// // const gatherPrompt = `\
// // asdasdas
// // `
// // const filterPrompt = `\
// // Is this file relevant?
// // `
export type SearchAndReplaceBlock = {
search: string;
replace: string;
}
// // // optimizations (DO THESE LATER!!!!!!)
// // // if tool includes a uri in uriSet, skip it obviously
// // let uriSet = new Set<URI>()
// // // gather
// // let messages = []
// // while (true) {
// // const result = await new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // onFinalMessage: ({ result: r, }) => {
// // res(r)
// // },
// // onError: (error) => {
// // rej(error)
// // }
// // })
// // })
// service that manages state
export type ApplyState = {
[applyBoxId: string]: {
searchAndReplaceBlocks: SearchAndReplaceBlock;
}
}
// // messages.push({ role: 'tool', content: turnToString(result) })
// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion`
// // sendLLMMessage({
// // messages: { 'Output ': result },
// // onFinalMessage: (r) => {
// // // output is file1\nfile2\nfile3\n...
// // }
// // })
export interface IFastApplyService {
readonly _serviceBrand: undefined;
// // uriSet.add(...)
// // }
// readonly state: ApplyState; // readonly to the user
// setState(newState: Partial<ApplyState>): void;
// onDidChangeState: Event<void>;
}
// // // writes
// // if (!replaceClause) return
export const IVoidFastApplyService = createDecorator<IFastApplyService>('voidFastApplyService');
class VoidFastApplyService extends Disposable implements IFastApplyService {
_serviceBrand: undefined;
// static readonly ID = 'voidFastApplyService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
// state
// state: ApplyState
constructor(
// @IToolService private readonly toolService: ToolService
) {
super()
// initial state
// this.state = { currentUri: undefined }
}
setState(newState: Partial<ApplyState>) {
// this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
aiSearch(searchStr: string) {
}
aiReplace(searchStr: string, replaceStr: string) {
}
// 1. search(ai)
// - tool use to find all possible changes
// - if search only: is this file related to the search?
// - if search + replace: should I modify this file?
// 2. replace(ai)
// - what changes to make?
// 3. postprocess errors
// -fastapply changes simultaneously
// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error)
// private async _searchUsingAI({ searchClause }: { searchClause: string }) {
// // const relevantURIs: URI[] = []
// // const gatherPrompt = `\
// // asdasdas
// // `
// // const filterPrompt = `\
// // Is this file relevant?
// // `
// // // optimizations (DO THESE LATER!!!!!!)
// // // if tool includes a uri in uriSet, skip it obviously
// // let uriSet = new Set<URI>()
// // // gather
// // let messages = []
// // while (true) {
// // const result = await new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // onFinalMessage: ({ result: r, }) => {
// // res(r)
// // },
// // onError: (error) => {
// // rej(error)
// // }
// // })
// // })
// // messages.push({ role: 'tool', content: turnToString(result) })
// // sendLLMMessage({
// // messages: { 'Output ': result },
// // onFinalMessage: (r) => {
// // // output is file1\nfile2\nfile3\n...
// // }
// // })
// // uriSet.add(...)
// // }
// // // writes
// // if (!replaceClause) return
// // for (const uri of uriSet) {
// // // in future, batch these
// // applyWorkflow({ uri, applyStr: replaceClause })
// // }
// // for (const uri of uriSet) {
// // // in future, batch these
// // applyWorkflow({ uri, applyStr: replaceClause })
// // }
// // while (true) {
// // const result = new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // onResult: (r) => {
// // res(r)
// // }
// // })
// // })
// // while (true) {
// // const result = new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // onResult: (r) => {
// // res(r)
// // }
// // })
// // })
// // messages.push(result)
// // messages.push(result)
// // }
// // }
// }
// }
// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) {
// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) {
// for (const uri of relevantURIs) {
// for (const uri of relevantURIs) {
// uri
// uri
// }
// }
// // should I change this file?
// // if so what changes to make?
// // should I change this file?
// // if so what changes to make?
// // fast apply the changes
// }
// // fast apply the changes
// }
}
registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager);
@@ -132,6 +132,7 @@ export type StartApplyingOpts = {
from: 'ClickApply';
type: 'searchReplace' | 'rewrite';
applyStr: string;
uri: 'current' | URI;
}
@@ -1226,6 +1227,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// throws if there's an error
public startApplying(opts: StartApplyingOpts) {
if (opts.type === 'rewrite') {
const addedDiffArea = this._initializeWriteoverStream(opts)
@@ -1400,7 +1402,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
// state used in onText:
let fullText = ''
let fullTextSoFar = '' // so far (INCLUDING ignored suffix)
let prevIgnoredSuffix = ''
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
@@ -1408,12 +1410,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K',
logging: { loggingName: `startApplying - ${from}` },
messages,
onText: ({ newText: newText_ }) => {
onText: ({ fullText: fullText_ }) => {
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix!
fullText += prevIgnoredSuffix + newText // full text, including ```, etc
fullTextSoFar += newText // full text, including ```, etc
const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length)
const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullTextSoFar, newText.length)
const { endLineInLlmTextSoFar } = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable)
diffZone._streamState.line = (diffZone.startLine - 1) + endLineInLlmTextSoFar // change coordinate systems from originalCode to full file
@@ -1447,11 +1450,18 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) {
const { applyStr } = opts
const { applyStr, uri: givenURI } = opts
let uri: URI
if (givenURI === 'current') {
const uri_ = this._getActiveEditorURI()
if (!uri_) return
uri = uri_
}
else {
uri = givenURI
}
const uri_ = this._getActiveEditorURI()
if (!uri_) return
const uri = uri_
// generate search/replace block text
const originalFileCode = this._voidFileService.readModel(uri)
@@ -173,7 +173,7 @@ export type ExtractedSearchReplaceBlock = {
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
// for each prefix
for (let i = anyPrefix.length; i >= 0; i--) {
for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string
const prefix = anyPrefix.slice(0, i)
if (str.endsWith(prefix)) return prefix
}
@@ -251,86 +251,109 @@ export const extractSearchReplaceBlocks = (str: string) => {
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
export const extractReasoningFromText = (
onText_: OnText,
thinkTags: [string, string],
): OnText => {
let latestAddIdx = 0 // exclusive
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
let latestAddIdx = 0 // exclusive index in fullText_
let foundTag1 = false
let foundTag2 = false
let fullText = ''
let fullReasoning = ''
let fullTextSoFar = ''
let fullReasoningSoFar = ''
const onText: OnText = ({ newText: newText_, fullText: fullText_ }) => {
// abcdef<t|hin|k>ghi
// |
let onText_ = onText
onText = (params) => {
onText_(params)
}
const newOnText: OnText = ({ fullText: fullText_ }) => {
// until found the first think tag, keep adding to fullText
if (!foundTag1) {
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
if (endsWithTag1) {
// console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ })
// wait until we get the full tag or know more
return
}
// if found the first tag
const tag1Index = fullText_.lastIndexOf(thinkTags[0])
const tag1Index = fullText_.indexOf(thinkTags[0])
if (tag1Index !== -1) {
// console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ })
foundTag1 = true
const newText = fullText.substring(latestAddIdx, tag1Index)
const newReasoning = fullText.substring(tag1Index + thinkTags[0].length, Infinity)
fullText += newText
fullReasoning += newReasoning
latestAddIdx += newText.length + newReasoning.length
onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning })
// Add text before the tag to fullTextSoFar
fullTextSoFar += fullText_.substring(0, tag1Index)
// Update latestAddIdx to after the first tag
latestAddIdx = tag1Index + thinkTags[0].length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar })
// add the text to fullText
const newText = fullText.substring(latestAddIdx, Infinity)
fullText += newText
latestAddIdx += newText.length
onText_({ newText, fullText, newReasoning: '', fullReasoning })
fullTextSoFar = fullText_
latestAddIdx = fullText_.length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag1>
// until found the second think tag, keep adding to fullReasoning
if (!foundTag2) {
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
if (endsWithTag2) {
// console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar })
// wait until we get the full tag or know more
return
}
// if found the second tag
const tag2Index = fullText_.lastIndexOf(thinkTags[1])
if (tag2Index !== -1) {
foundTag2 = true
const newReasoning = fullText.substring(latestAddIdx, tag2Index)
const newText = fullText.substring(tag2Index + thinkTags[1].length, Infinity)
fullText += newText
fullReasoning += newReasoning
latestAddIdx += newText.length + newReasoning.length
onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning })
// if found the second tag
const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx)
if (tag2Index !== -1) {
// console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar })
foundTag2 = true
// Add everything between first and second tag to reasoning
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
// Update latestAddIdx to after the second tag
latestAddIdx = tag2Index + thinkTags[1].length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// add the text to fullReasoning
const newReasoning = fullText.substring(latestAddIdx, Infinity)
fullReasoning += newReasoning
latestAddIdx += newReasoning.length
onText_({ newText: '', fullText, newReasoning, fullReasoning })
// add the text to fullReasoning (content after first tag but before second tag)
// console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar })
// If we have more text than we've processed, add it to reasoning
if (fullText_.length > latestAddIdx) {
fullReasoningSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
// at this point, we found <tag2>
fullText += newText_
const newText = fullText.substring(latestAddIdx, Infinity)
latestAddIdx += newText.length
onText_({ newText, fullText, newReasoning: '', fullReasoning })
// at this point, we found <tag2> - content after the second tag is normal text
// console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar })
// Add any new text after the closing tag to fullTextSoFar
if (fullText_.length > latestAddIdx) {
fullTextSoFar += fullText_.substring(latestAddIdx)
latestAddIdx = fullText_.length
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
return onText
return newOnText
}
export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => {
const tag1Idx = fullText_.indexOf(thinkTags[0])
const tag2Idx = fullText_.indexOf(thinkTags[1])
if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning
if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning
const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx)
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
return { fullText, fullReasoning }
}
@@ -6,7 +6,7 @@
import { URI } from '../../../../../base/common/uri.js';
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
import { CodeSelection, StagingSelectionItem, FileSelection } from '../../common/chatThreadService.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { os } from '../helpers/systemInfo.js';
import { IVoidFileService } from '../../common/voidFileService.js';
@@ -15,6 +15,14 @@ import { IVoidFileService } from '../../common/voidFileService.js';
// this is just for ease of readability
export const tripleTick = ['```', '```']
export const editToolDesc_toolDescription = `\
A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \
Typically the best description you can give here is a high level view of the final code you'd like to see. For example, code excerpt(s) with "// ... existing code ..." comments to help you write less. \
However, you are allowed to describe the change using whatever text/language you like, especially if the change is better described without code. \
Do NOT output the whole file if possible, and try to write as LITTLE as needed to describe the change.`
export const chat_systemMessage = (workspaces: string[]) => `\
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.
@@ -299,7 +307,7 @@ For example, if the user is asking you to "make this variable a better name", ma
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => {
// we may want to do this in batches
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null }
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } }
const file = await stringifyFileSelections([fileSelection], voidFileService)
@@ -86,6 +86,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
from: 'ClickApply',
type: 'searchReplace',
applyStr: codeStr,
uri: 'current',
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1)
@@ -13,7 +13,7 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
return (
<>
<div className="relative group w-full overflow-hidden my-4">
<div className="relative group w-full overflow-hidden">
{buttonsOnHover === null ? null : (
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}>
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
@@ -6,10 +6,14 @@
import React, { JSX } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { ChatMessageLocation, } from '../../../aiRegexService.js'
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
export type ChatMessageLocation = {
threadId: string;
messageIdx: number;
}
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
@@ -33,7 +37,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
</code>
}
const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
@@ -45,17 +49,19 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
if (t.type === "code") {
const applyBoxId = chatMessageLocation ? getApplyBoxId({
threadId: chatMessageLocation.threadId,
messageIdx: chatMessageLocation.messageIdx,
const applyBoxId = chatMessageLocationForApply ? getApplyBoxId({
threadId: chatMessageLocationForApply.threadId,
messageIdx: chatMessageLocationForApply.messageIdx,
tokenIdx: tokenIdx,
}) : null
return <BlockCode
return <div className='my-4'>
<BlockCode
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
/>
</div>
}
if (t.type === "heading") {
@@ -129,7 +135,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
)}
<span className="ml-1">
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={item.text} nested={true} />
</span>
</li>
))}
@@ -241,12 +247,12 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
)
}
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation }) => {
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocationForApply }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} />
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
))}
</>
)
@@ -7,10 +7,10 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js';
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadService.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdownRender.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
@@ -24,7 +24,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { ChevronRight, Pencil, X } from 'lucide-react';
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { ChatMessageLocation } from '../../../aiRegexService.js';
import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js';
@@ -139,6 +139,9 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
}
const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`;
interface VoidChatAreaProps {
// Required
children: React.ReactNode; // This will be the input component
@@ -148,7 +151,7 @@ interface VoidChatAreaProps {
onAbort: () => void;
isStreaming: boolean;
isDisabled?: boolean;
divRef?: React.RefObject<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement | null>;
// UI customization
featureName: FeatureName;
@@ -187,12 +190,15 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
return (
<div
ref={divRef}
// border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
className={`
flex flex-col gap-1 p-2 relative input text-left shrink-0
gap-1
flex flex-col p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
outline-1 outline-void-border-3 focus-within:outline-void-border-1 hover:outline-void-border-1
max-h-[80vh] overflow-y-auto
${className}
`}
onClick={(e) => {
@@ -370,12 +376,6 @@ export const SelectedFiles = (
| { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean }
) => {
// index -> isOpened
const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? [])
// state for tracking hover on clear all button
const [isClearHovered, setIsClearHovered] = useState(false)
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
@@ -403,6 +403,7 @@ export const SelectedFiles = (
fileURI: uri,
selectionStr: null,
range: null,
state: { isOpened: false },
}))
}
@@ -413,106 +414,96 @@ export const SelectedFiles = (
}
return (
<div className='flex items-center flex-wrap text-left relative'>
<div className='flex items-center flex-wrap text-left relative gap-x-0.5 gap-y-1'>
{allSelections.map((selection, i) => {
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
const isThisSelectionOpened = (!!selection.selectionStr && selection.state.isOpened) //!!(selection.selectionStr && selectionIsOpened[i])
const isThisSelectionAFile = selection.selectionStr === null
const isThisSelectionProspective = i > selections.length - 1
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
const selectionHTML = (<div key={thisKey} // container for `selectionSummary` and `selectionText`
return <div // container for summarybox and code
key={thisKey}
className={`
flex flex-col space-y-[1px]
${isThisSelectionOpened ? 'w-full' : ''}
`}
>
{/* selection summary */}
<div // container for item and its delete button (if it's last)
className='flex items-center gap-1 mr-0.5 my-0.5'
{/* summarybox */}
<div
className={`
flex items-center gap-0.5 relative
px-1
w-fit h-fit
select-none
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
text-xs text-nowrap
border rounded-sm ${isThisSelectionProspective
? 'border-void-border-2'
: isThisSelectionOpened
? 'border-void-border-1 ring-1 ring-[#007FD4]'
: 'border-void-border-1'
}
hover:border-void-border-1
transition-all duration-150
`}
onClick={() => {
if (type !== 'staging') return; // (never)
if (isThisSelectionProspective) { // add prospective selection to selections
setSelections([...selections, selection])
} else if (isThisSelectionAFile) { // open files
commandService.executeCommand('vscode.open', selection.fileURI, {
preview: true,
// preserveFocus: false,
});
} else { // show text
const selection = selections[i]
const newSelection = { ...selection, state: { isOpened: !selection.state.isOpened } }
const newSelections = [
...selections.slice(0, i),
newSelection,
...selections.slice(i + 1)
]
setSelections(newSelections)
// setSelectionIsOpened(s => {
// const newS = [...s]
// newS[i] = !newS[i]
// return newS
// });
}
}}
>
<div // styled summary box
className={`flex items-center gap-0.5 relative
px-1
w-fit h-fit
select-none
${isThisSelectionProspective ? 'bg-void-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
text-xs text-nowrap
border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
transition-all duration-150`}
onClick={() => {
if (isThisSelectionProspective) { // add prospective selection to selections
if (type !== 'staging') return; // (never)
setSelections([...selections, selection])
{ // file name and range
getBasename(selection.fileURI.fsPath)
+ (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`)
}
} else if (isThisSelectionAFile) { // open files
commandService.executeCommand('vscode.open', selection.fileURI, {
preview: true,
// preserveFocus: false,
});
} else { // show text
setSelectionIsOpened(s => {
const newS = [...s]
newS[i] = !newS[i]
return newS
});
}
}}
>
<span>
{/* file name */}
{getBasename(selection.fileURI.fsPath)}
{/* selection range */}
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
</span>
{/* X button */}
{type === 'staging' && !isThisSelectionProspective &&
<span
className='cursor-pointer z-1'
onClick={(e) => {
e.stopPropagation(); // don't open/close selection
if (type !== 'staging') return;
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
}}
>
<IconX size={10} className="stroke-[2]" />
</span>}
</div>
{/* clear all selections button */}
{/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1
? null
: <div className={`flex items-center ${isThisSelectionOpened ? 'w-full' : ''}`}>
<div
className='rounded-md'
onMouseEnter={() => setIsClearHovered(true)}
onMouseLeave={() => setIsClearHovered(false)}
>
<Delete
size={16}
className={`stroke-[1]
stroke-void-fg-1
fill-void-bg-3
opacity-40
hover:opacity-60
transition-all duration-150
cursor-pointer
`}
onClick={() => { setSelections([]) }}
/>
</div>
</div>
} */}
{type === 'staging' && !isThisSelectionProspective ? // X button
<IconX
className='cursor-pointer z-1 stroke-[2]'
onClick={(e) => {
e.stopPropagation(); // don't open/close selection
if (type !== 'staging') return;
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
}}
size={10}
/>
: <></>
}
</div>
{/* selection text */}
{isThisSelectionOpened &&
{/* code box */}
{isThisSelectionOpened ?
<div
className='w-full px-1 rounded-sm border-vscode-editor-border'
className={`
w-full px-1 rounded-sm border-vscode-editor-border
${isThisSelectionOpened ? 'ring-1 ring-[#007FD4]' : ''}
`}
onClick={(e) => {
e.stopPropagation(); // don't focus input box
}}
@@ -524,14 +515,9 @@ export const SelectedFiles = (
showScrollbars={true}
/>
</div>
: <></>
}
</div>)
return <Fragment key={thisKey}>
{/* divider between `selections` and `prospectiveSelections` */}
{/* {selections.length > 0 && i === selections.length && <div className='w-full'></div>} */}
{selectionHTML}
</Fragment>
</div>
})}
@@ -542,39 +528,59 @@ export const SelectedFiles = (
}
type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
interface ToolResultProps {
actionTitle: string;
actionParam: string;
actionNumResults?: number;
children?: React.ReactNode;
const actionTitleOfToolName: { [T in ToolName]: string } = {
'read_file': 'Read file',
'list_dir': 'Inspected folder',
'pathname_search': 'Searched filename',
'search': 'Searched',
'create_uri': 'Created URI',
'delete_uri': 'Deleted URI',
'edit': 'Edited file',
'terminal_command': 'Ran terminal command',
}
type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
const ToolResult = ({
actionTitle,
toolName,
actionParam,
actionNumResults,
children,
}: ToolResultProps) => {
onClick,
}: {
toolName: ToolName;
actionParam: string;
actionNumResults?: number;
children?: React.ReactNode;
onClick?: () => void;
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const isDropdown = !!children
const isClickable = !!isDropdown || !!onClick
return (
<div className="mx-4 select-none">
<div className="border border-void-border-3 rounded px-1 py-0.5 bg-void-bg-tool">
<div className="border border-void-border-3 rounded px-2 py-1 bg-void-bg-2-alt overflow-hidden">
<div
className={`flex items-center min-h-[24px] ${isDropdown ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : 'mx-1'}`}
onClick={() => children && setIsExpanded(!isExpanded)}
className={`flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
onClick={() => {
if (children) { setIsExpanded(v => !v); }
if (onClick) { onClick(); }
}}
>
{isDropdown && (
<ChevronRight
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
/>
)}
<div className="flex items-center flex-wrap gap-x-2 gap-y-0.5">
<span className="text-void-fg-3">{actionTitle}</span>
<span className="text-void-fg-4 text-xs italic">{`"`}{actionParam}{`"`}</span>
<div className="flex items-center flex-nowrap whitespace-nowrap gap-x-2">
<span className="text-void-fg-3">{actionTitleOfToolName[toolName]}</span>
<span className="text-void-fg-4 text-xs italic">{actionParam}</span>
{actionNumResults !== undefined && (
<span className="text-void-fg-4 text-xs">
{`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`}
@@ -583,7 +589,8 @@ const ToolResult = ({
</div>
</div>
<div
className={`mt-1 overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}
// the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition.
className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`}
>
{children}
</div>
@@ -594,98 +601,246 @@ const ToolResult = ({
const toolResultToComponent: ToolReusltToComponent = {
'read_file': ({ message }) => (
<ToolResult
actionTitle="Read file"
actionParam={getBasename(message.result.uri.fsPath)}
/>
),
'list_dir': ({ message }) => (
<ToolResult
actionTitle="Inspected folder"
actionParam={`${getBasename(message.result.rootURI.fsPath)}/`}
actionNumResults={message.result.children?.length}
>
<div className="text-void-fg-2">
{message.result.children?.map((item, i) => (
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
{item.name}
{item.isDirectory && '/'}
const ToolError = <T extends ToolName,>({ toolName, errorMessage }: { toolName: T, errorMessage: string }) => {
return <ToolResult
toolName={toolName}
actionParam={errorMessage}
/>
}
const toolResultToComponent: ToolResultToComponent = {
'read_file': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='read_file' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='read_file'
actionParam={'View file'}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{getBasename(value.uri.fsPath)}
</div>
))}
{message.result.hasNextPage && (
<div className="pl-2 text-void-fg-3 italic">
{message.result.itemsRemaining} more items...
</div>
)}
</div>
</ToolResult>
),
'pathname_search': ({ message }) => (
<ToolResult
actionTitle="Searched filename"
actionParam={message.result.queryStr}
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
>
<div className="text-void-fg-2">
{Array.isArray(message.result.uris) ?
message.result.uris.map((uri, i) => (
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
<a
href={uri.toString()}
className="text-void-accent hover:underline"
>
{uri.fsPath.split('/').pop()}
</a>
{value.hasNextPage && (<div className="italic">AI can scroll for more content...</div>)}
</div>
</ToolResult>
)
},
'list_dir': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const explorerService = accessor.get('IExplorerService')
// message.result.hasNextPage = true
// message.result.itemsRemaining = 400
if (message.result.type === 'error') return <ToolError toolName='list_dir' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='list_dir'
actionParam={`${getBasename(value.rootURI.fsPath)}/`}
actionNumResults={value.children?.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{value.children?.map((child, i) => (
<div key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => {
commandService.executeCommand('workbench.view.explorer');
explorerService.select(child.uri, true);
}}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{`${child.name}${child.isDirectory ? '/' : ''}`}
</div>
)) :
<div className="pl-2">{message.result.uris}</div>
}
{message.result.hasNextPage && (
<div className="pl-2 text-void-fg-3 italic">
More results available...
</div>
)}
</div>
</ToolResult>
),
'search': ({ message }) => (
<ToolResult
actionTitle="Searched"
actionParam={message.result.queryStr}
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
>
<div className="text-void-fg-2">
{typeof message.result.uris === 'string' ?
message.result.uris :
message.result.uris.map((uri, i) => (
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
<a
href={uri.toString()}
className="text-void-accent hover:underline"
>
{uri.fsPath}
</a>
))}
{value.hasNextPage && (<div className="italic">{value.itemsRemaining} more items...</div>)}
</div>
</ToolResult>
)
},
'pathname_search': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='pathname_search' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='pathname_search'
actionParam={`"${value.queryStr}"`}
actionNumResults={value.uris.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{value.uris.map((uri, i) => (
<div key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{uri.fsPath.split('/').pop()}
</div>
))
}
{message.result.hasNextPage && (
<div className="pl-2 text-void-fg-3 italic">
More results available...
))}
{value.hasNextPage && (<div className="italic">More results available...</div>)}
</div>
</ToolResult>
)
},
'search': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='search' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='search'
actionParam={`"${value.queryStr}"`}
actionNumResults={value.uris.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{value.uris.map((uri, i) => (
<div key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{uri.fsPath.split('/').pop()}
</div>
))}
{value.hasNextPage && (<div className="italic">More results available...</div>)}
</div>
</ToolResult>
)
},
// ---
'create_uri': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='create_uri' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='create_uri'
actionParam={getBasename(value.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
</div>
)}
</div>
</ToolResult>
)
</div>
</ToolResult>
)
},
'delete_uri': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='delete_uri' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='delete_uri'
actionParam={getBasename(value.uri.fsPath) + ' (deleted)'}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
)
},
'edit': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='edit' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='edit'
actionParam={getBasename(value.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
)
},
'terminal_command': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='terminal_command' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='terminal_command'
actionParam={value.command}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
// TODO!!! open terminal
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
<ChatMarkdownRender string={''} />
</div>
</div>
</ToolResult>
)
}
};
type ChatBubbleMode = 'display' | 'edit'
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, }) => {
const role = chatMessage.role
// Only show reasoning dropdown when there's actual content
const reasoningStr = (chatMessage.role === 'assistant' && chatMessage.reasoning?.trim()) || null
const hasReasoning = !!reasoningStr
const [isReasoningOpen, setIsReasoningOpen] = useState(false)
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@@ -720,7 +875,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current
if (canInitialize && shouldInitialize) {
setStagingSelections(chatMessage.selections || [])
if (textAreaFnsRef.current)
textAreaFnsRef.current.setValue(chatMessage.displayContent || '')
@@ -750,7 +904,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
if (mode === 'display') {
chatbubbleContents = <>
<SelectedFiles type='past' selections={chatMessage.selections || []} />
{chatMessage.displayContent}
<span className='px-0.5'>{chatMessage.displayContent}</span>
</>
}
else if (mode === 'edit') {
@@ -806,7 +960,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
>
<VoidInputBox2
ref={setTextAreaRef}
className='min-h-[81px] max-h-[500px] p-1'
className='min-h-[81px] max-h-[500px] px-0.5'
placeholder="Edit your message..."
onChangeText={(text) => setIsDisabled(!text)}
onFocus={() => {
@@ -829,10 +983,42 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
const chatMessageLocation: ChatMessageLocation = {
threadId: thread.id,
messageIdx: messageIdx!,
messageIdx: messageIdx,
}
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatMessageLocation={chatMessageLocation} />
const reasoningDropdown = hasReasoning ? (
<div className="mx-4 select-none mt-2">
<div className="border border-void-border-3 rounded px-1 py-0.5 bg-void-bg-tool">
<div
className="flex items-center min-h-[24px] cursor-pointer hover:brightness-125 transition-all duration-150"
onClick={() => setIsReasoningOpen(!isReasoningOpen)}
>
<ChevronRight
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isReasoningOpen ? 'rotate-90' : ''}`}
/>
<div className="flex items-center flex-wrap gap-x-2 gap-y-0.5">
<span className="text-void-fg-3">Reasoning</span>
<span className="text-void-fg-4 text-xs italic">Model's step-by-step thinking</span>
</div>
</div>
<div
className={`mt-1 overflow-hidden transition-all duration-200 ease-in-out ${isReasoningOpen ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}
>
<div className="text-void-fg-2 p-2 bg-void-bg-1 rounded">
<ChatMarkdownRender string={reasoningStr} chatMessageLocationForApply={chatMessageLocation} />
</div>
</div>
</div>
</div>
) : null
chatbubbleContents = (<>
{/* Reasoning dropdown (conditional) */}
{reasoningDropdown}
{/* Main content */}
<ChatMarkdownRender string={chatMessage.content ?? ''} chatMessageLocationForApply={chatMessageLocation} />
</>)
}
else if (role === 'tool') {
@@ -849,7 +1035,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
className={`
relative
${mode === 'edit' ? 'px-2 w-full max-w-full'
: role === 'user' ? `my-0.5 px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
}
`}
@@ -862,7 +1048,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
text-left rounded-lg
max-w-full
${mode === 'edit' ? ''
: role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
: role === 'user' ? 'p-2 flex flex-col gap-1 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
: role === 'assistant' ? 'px-2 overflow-x-auto' : ''
}
`}
@@ -926,14 +1112,15 @@ export const SidebarChat = () => {
const currentThread = chatThreadsService.getCurrentThread()
const previousMessages = currentThread?.messages ?? []
const selections = chatThreadsService.getCurrentThread().state.stagingSelections
const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) }
const selections = currentThread.state.stagingSelections
const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadState({ stagingSelections: s }) }
// stream state
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
const isStreaming = !!currThreadStreamState?.streamingToken
const latestError = currThreadStreamState?.error
const messageSoFar = currThreadStreamState?.messageSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
// ----- SIDEBAR CHAT state (local) -----
@@ -982,13 +1169,27 @@ export const SidebarChat = () => {
}, [isHistoryOpen, currentThread.id])
const prevMessagesHTML = useMemo(() => {
const pastMessagesHTML = useMemo(() => {
return previousMessages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} messageIdx={i} />
<ChatBubble key={getChatBubbleId(currentThread.id, i)} chatMessage={message} messageIdx={i} />
)
}, [previousMessages])
const streamingChatIdx = pastMessagesHTML.length
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ?
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
messageIdx={streamingChatIdx} chatMessage={{
role: 'assistant',
content: messageSoFar ?? null,
reasoning: reasoningSoFar ?? null,
}}
isLoading={isStreaming}
/> : null
const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML]
const threadSelector = <div ref={historyRef}
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
>
@@ -1006,15 +1207,12 @@ export const SidebarChat = () => {
overflow-x-hidden
overflow-y-auto
py-4
${prevMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
${pastMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
`}
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
>
{/* previous messages */}
{prevMessagesHTML}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isStreaming} />
{allMessagesHTML}
{/* error message */}
@@ -1049,14 +1247,14 @@ export const SidebarChat = () => {
isStreaming={isStreaming}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={prevMessagesHTML.length === 0}
showProspectiveSelections={pastMessagesHTML.length === 0}
selections={selections}
setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
featureName="Ctrl+L"
>
<VoidInputBox2
className='min-h-[81px] p-1'
className='min-h-[81px] px-0.5'
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
@@ -310,6 +310,7 @@ export const VoidCustomDropdownBox = <T extends any>({
selectedOption,
onChangeOption,
getOptionDropdownName,
getOptionDropdownDetail,
getOptionDisplayName,
getOptionsEqual,
className,
@@ -321,6 +322,7 @@ export const VoidCustomDropdownBox = <T extends any>({
selectedOption: T | undefined;
onChangeOption: (newValue: T) => void;
getOptionDropdownName: (option: T) => string;
getOptionDropdownDetail?: (option: T) => string;
getOptionDisplayName: (option: T) => string;
getOptionsEqual: (a: T, b: T) => boolean;
className?: string;
@@ -420,12 +422,21 @@ export const VoidCustomDropdownBox = <T extends any>({
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
aria-hidden="true"
>
{options.map((option) => (
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
<div className="w-4" />
<span className="px-2">{getOptionDropdownName(option)}</span>
</div>
))}
{options.map((option) => {
const optionName = getOptionDropdownName(option);
const optionDetail = getOptionDropdownDetail?.(option) || '';
return (
<div key={optionName + optionDetail} className="flex items-center whitespace-nowrap">
<div className="w-4" />
<span className="flex justify-between w-full">
<span>{optionName}</span>
<span>{optionDetail}</span>
<span>______</span>
</span>
</div>
)
})}
</div>
{/* Select Button */}
@@ -473,6 +484,7 @@ export const VoidCustomDropdownBox = <T extends any>({
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
const optionName = getOptionDropdownName(option);
const optionDetail = getOptionDropdownDetail?.(option) || '';
return (
<div
@@ -500,7 +512,10 @@ export const VoidCustomDropdownBox = <T extends any>({
</svg>
)}
</div>
<span>{optionName}</span>
<span className="flex justify-between w-full">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>
</div>
);
})}
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect, useCallback } from 'react'
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
import { ThreadStreamState,IChatThreadService, ThreadsState } from '../../../../common/chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
@@ -15,6 +15,7 @@ import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
import { IExplorerService } from '../../../../../../../workbench/contrib/files/browser/files.js'
import { IModelService } from '../../../../../../../editor/common/services/model.js';
import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js';
import { IContextViewService, IContextMenuService } from '../../../../../../../platform/contextview/browser/contextView.js';
@@ -28,7 +29,6 @@ import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
import { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IChatThreadService } from '../../../chatThreadService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
@@ -226,6 +226,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
IKeybindingService: accessor.get(IKeybindingService),
IExplorerService: accessor.get(IExplorerService),
IEnvironmentService: accessor.get(IEnvironmentService),
IConfigurationService: accessor.get(IConfigurationService),
IPathService: accessor.get(IPathService),
@@ -37,7 +37,8 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
selectedOption={selectedOption}
onChangeOption={onChangeOption}
getOptionDisplayName={(option) => option.selection.modelName}
getOptionDropdownName={(option) => option.name}
getOptionDropdownName={(option) => option.selection.modelName}
getOptionDropdownDetail={(option) => option.selection.providerName }
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
className='text-xs text-void-fg-3 px-1'
matchInputWidth={false}
@@ -46,7 +46,7 @@ module.exports = {
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
"void-border-2": "var(--vscode-commandCenter-border)",
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
"void-border-3": "var(--vscode-settings-sashBorder)",
"void-border-4": "var(--vscode-editorGroup-border)",
vscode: {
@@ -7,8 +7,8 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
// import { ILLMMessageService } from '../common/llmMessageService.js';
// import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
@@ -24,22 +24,22 @@ class SearchReplaceService extends Disposable implements ISearchReplaceService {
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
constructor(
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
// @ILLMMessageService private readonly llmMessageService: ILLMMessageService,
) {
super()
}
send(params: Omit<ServiceSendLLMMessageParams, 'onText'> & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
this.llmMessageService.sendLLMMessage({
...params as ServiceSendLLMMessageParams,
onText: (p) => {
const { retry } = params.onText(p)
if (retry) {
// send(params: ServiceSendLLMMessageParams & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
// this.llmMessageService.sendLLMMessage({
// ...params as ServiceSendLLMMessageParams,
// onText: (p) => {
// const { retry } = params.onText(p)
// if (retry) {
}
}
})
}
// }
// }
// })
// }
}
@@ -11,7 +11,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js';
import { StagingSelectionItem, IChatThreadService } from '../common/chatThreadService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
@@ -124,11 +124,13 @@ registerAction2(class extends Action2 {
fileURI: model.uri,
selectionStr: null,
range: null,
state: { isOpened: false, }
} : {
type: 'Selection',
fileURI: model.uri,
selectionStr: selectionStr,
range: selectionRange,
state: { isOpened: true, }
}
// update the staging selections
@@ -141,13 +143,16 @@ registerAction2(class extends Action2 {
let setSelections = (s: StagingSelectionItem[]) => { }
if (focusedMessageIdx === undefined) {
selections = chatThreadService.getCurrentThreadStagingSelections()
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s)
selections = chatThreadService.getCurrentThreadState().stagingSelections
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadState({ stagingSelections: s })
} else {
selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
}
// close all selections besides the new one
selections = selections.map(s => ({ ...s, state: { ...s.state, isOpened: false } }))
// if matches with existing selection, overwrite (since text may change)
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
@@ -0,0 +1,71 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { generateUuid } from '../../../../base/common/uuid.js';
export interface ITerminalToolService {
readonly _serviceBrand: undefined;
createNewTerminal(terminalId: string): Promise<string>;
runCommand(command: string, terminalId?: string): Promise<void>;
focus(terminalId: string): Promise<void>;
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
export class TerminalToolService extends Disposable implements ITerminalToolService {
readonly _serviceBrand: undefined;
private terminalInstances: Record<string, ITerminalInstance> = {}
constructor(
@ITerminalService private readonly terminalService: ITerminalService
) {
super();
}
async createNewTerminal() {
const terminalId = generateUuid();
this.terminalService.createTerminal({});
const terminal = await this.terminalService.createTerminal({
location: TerminalLocation.Editor,
config: { name: `Void Agent (${terminalId})`, }
});
this.terminalInstances[terminalId] = terminal
return terminalId;
}
async runCommand(command: string, terminalId?: string) {
if (!terminalId) {
terminalId = await this.createNewTerminal();
}
const terminal = this.terminalInstances[terminalId];
if (!terminal) throw new Error(`Terminal with ID ${terminalId} does not exist`);
terminal.sendText(command, true);
return;
}
async focus(terminalId: string) {
const terminal = this.terminalInstances[terminalId];
if (!terminal) throw new Error(`That terminal was closed.`);
terminal.focus(true);
return;
}
}
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager);
@@ -15,8 +15,6 @@ import './sidebarStateService.js'
// register quick edit (Ctrl+K)
import './quickEditActions.js'
// register Thread History
import './chatThreadService.js'
// register Autocomplete
import './autocompleteService.js'
@@ -56,3 +54,7 @@ import '../common/voidUpdateService.js'
// tools
import '../common/toolsService.js'
// register Thread History
import '../common/chatThreadService.js'
@@ -11,13 +11,14 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js';
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js';
import { toLLMChatMessage } from '../common/llmMessageTypes.js';
import { ILLMMessageService } from './llmMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js';
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolName, voidTools } from './toolsService.js';
import { toLLMChatMessage, ToolCallType } from './llmMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IVoidFileService } from '../common/voidFileService.js';
import { IVoidFileService } from './voidFileService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { getErrorMessage } from '../../../../base/common/errors.js';
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
@@ -36,6 +37,9 @@ export type CodeSelection = {
fileURI: URI;
selectionStr: string;
range: IRange;
state: {
isOpened: boolean;
};
}
export type FileSelection = {
@@ -43,6 +47,9 @@ export type FileSelection = {
fileURI: URI;
selectionStr: null;
range: null;
state: {
isOpened: boolean;
};
}
export type StagingSelectionItem = CodeSelection | FileSelection
@@ -53,18 +60,12 @@ export type ToolMessage<T extends ToolName> = {
name: T; // internal use
params: string; // internal use
id: string; // apis require this tool use id
content: string; // result
result: ToolCallReturnType[T]; // text message of result
content: string; // give this result to LLM
result: { type: 'success'; value: ToolCallReturnType[T], } | { type: 'error'; value: string }; // give this result to user
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'system';
content: string;
displayContent?: undefined;
} | {
{
role: 'user';
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
@@ -76,7 +77,7 @@ export type ChatMessage =
} | {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking
}
| ToolMessage<ToolName>
@@ -98,14 +99,18 @@ export type ChatThreads = {
state: {
stagingSelections: StagingSelectionItem[];
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
isCheckedOfSelectionId: { [selectionId: string]: boolean };
isCheckedOfSelectionId: { [selectionId: string]: boolean }; // TODO
}
};
}
type ThreadType = ChatThreads[string]
const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} }
const defaultThreadState: ThreadType['state'] = {
stagingSelections: [],
focusedMessageIdx: undefined,
isCheckedOfSelectionId: {}
}
export type ThreadsState = {
allThreads: ChatThreads;
@@ -116,6 +121,7 @@ export type ThreadStreamState = {
[threadId: string]: undefined | {
error?: { message: string, fullError: Error | null, };
messageSoFar?: string;
reasoningSoFar?: string;
streamingToken?: string;
}
}
@@ -128,19 +134,12 @@ const newThreadObject = () => {
createdAt: now,
lastModified: now,
messages: [],
state: {
stagingSelections: [],
focusedMessageIdx: undefined,
isCheckedOfSelectionId: {}
},
state: defaultThreadState,
} satisfies ChatThreads[string]
}
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
const LATEST_THREAD_VERSION = 'v2'
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
export const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
type ChatMode = 'agent' | 'chat'
@@ -166,8 +165,8 @@ export interface IChatThreadService {
// exposed getters/setters
getCurrentMessageState: (messageIdx: number) => UserMessageState
setCurrentMessageState: (messageIdx: number, newState: Partial<UserMessageState>) => void
getCurrentThreadStagingSelections: () => StagingSelectionItem[]
setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void
getCurrentThreadState: () => ThreadType['state']
setCurrentThreadState: (newState: Partial<ThreadType['state']>) => void
// call to edit a message
@@ -203,18 +202,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
const oldVersionNum = this._storageService.get(THREAD_VERSION_KEY, StorageScope.APPLICATION)
const readThreads = this._readAllThreads() || {}
const readThreads = this._readAllThreads()
const updatedThreads = this._updatedThreadsToVersion(readThreads, oldVersionNum)
if (updatedThreads !== null) {
this._storeAllThreads(updatedThreads)
}
const allThreads = updatedThreads ?? readThreads
const allThreads = readThreads
this.state = {
allThreads: allThreads,
currentThreadId: null as unknown as string, // gets set in startNewThread()
@@ -222,65 +214,37 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// always be in a thread
this.openNewThread()
this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
}
private _readAllThreads(): ChatThreads {
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {}
return threads
}
// returns if should update
private _updatedThreadsToVersion(oldThreadsObject: any, oldVersion: string | undefined): ChatThreads | null {
if (!oldVersion) {
// unknown, just reset chat?
return null
}
/** v1 -> v2
- threads.state.currentStagingSelections: CodeStagingSelection[] | null;
+ thread[threadIdx].state
+ message.state
+ chatMessage.staging: StagingInfo | null
*/
else if (oldVersion === 'v1') {
const threads = oldThreadsObject as Omit<ChatThreads, 'staging' | 'focusedMessageIdx'>
// update the threads
for (const thread of Object.values(threads)) {
if (!thread.state) {
thread.state = defaultThreadState
}
for (const chatMessage of Object.values(thread.messages)) {
if (chatMessage.role === 'user' && !chatMessage.state) {
chatMessage.state = defaultMessageState
}
}
// !!! this is important for properly restoring URIs from storage
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
return JSON.parse(threadsStr, (key, value) => {
if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI
return URI.from(value);
}
return value;
});
}
// push the update
return threads
}
else if (oldVersion === 'v2') {
private _readAllThreads(): ChatThreads | null {
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION);
if (!threadsStr) {
return null
}
// up to date
return null
return this._convertThreadDataFromStorage(threadsStr);
}
private _storeAllThreads(threads: ChatThreads) {
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
const serializedThreads = JSON.stringify(threads);
this._storageService.store(
THREAD_STORAGE_KEY,
serializedThreads,
StorageScope.APPLICATION,
StorageTarget.USER
);
}
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
this.state = {
@@ -313,10 +277,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// ---------- streaming ----------
private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
private _finishStreamingTextMessage = (threadId: string, options: { content: string, reasoning?: string }, error?: { message: string, fullError: Error | null }) => {
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null })
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
this._addMessageToThread(threadId, { role: 'assistant', content: options.content, reasoning: options.reasoning || null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error })
}
@@ -331,8 +295,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
// get prev and curr selections before clearing the message
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx)
const currSelns = thread.messages[messageIdx].selections || []
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) // selections for previous messages
const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
// clear messages up to the index
const slicedMessages = thread.messages.slice(0, messageIdx)
@@ -366,7 +330,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const instructions = userMessage
const userMessageContent = await chat_userMessageContent(instructions, currSelns)
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService)
const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr)
const userMessageFullContent = chat_userMessageContentWithAllFilesToo(userMessageContent, selectionsStr)
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
@@ -414,51 +378,60 @@ class ChatThreadService extends Disposable implements IChatThreadService {
tools: tools,
onText: ({ fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })
onText: ({ fullText, fullReasoning }) => {
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning })
},
onFinalMessage: async ({ fullText, toolCalls }) => {
onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => {
if ((toolCalls?.length ?? 0) === 0) {
this._finishStreamingTextMessage(threadId, fullText)
this._finishStreamingTextMessage(threadId, { content: fullText, reasoning: fullReasoning })
}
else {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText })
this._setStreamState(threadId, { messageSoFar: undefined }) // clear streaming message
for (const tool of toolCalls ?? []) {
const toolName = tool.name as ToolName
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message
// 1.
let toolResult: Awaited<ReturnType<ToolFns[ToolName]>>
let toolResultVal: ToolCallReturnType[ToolName]
try {
toolResult = await this._toolsService.toolFns[toolName](tool.params)
toolResultVal = toolResult
} catch (error) {
this._setStreamState(threadId, { error })
shouldSendAnotherMessage = false
break
}
// deal with the tool
const tool: ToolCallType | undefined = toolCalls?.[0]
if (!tool) {
res_()
return
}
const toolName = tool.name
// 2.
let toolResultStr: string
try {
toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
this._setStreamState(threadId, { error })
shouldSendAnotherMessage = false
break
}
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResultVal, })
// 1.
let toolResultVal: ToolCallReturnType[typeof toolName]
try {
const val = await this._toolsService.toolFns[toolName](tool.params)
toolResultVal = val
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
shouldSendAnotherMessage = true
res_()
return
}
// 2.
let toolResultStr: string
try {
toolResultStr = this._toolsService.toolResultToString[toolName](toolResultVal as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
// treat as irrecoverable error
this._setStreamState(threadId, { error })
res_()
return
}
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: { type: 'success', value: toolResultVal }, })
shouldSendAnotherMessage = true
res_()
}
res_()
},
onError: (error) => {
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }, error)
res_()
},
})
@@ -469,14 +442,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs
agentLoop() // DO NOT AWAIT THIS, add fn should resolve when we've added message (this lets us interrupt the agent loop correctly instead of waiting for it to resolve)
}
cancelStreaming(threadId: string) {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '')
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar })
}
dismissStreamError(threadId: string): void {
@@ -489,7 +464,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
getCurrentThread(): ChatThreads[string] {
const state = this.state
return state.allThreads[state.currentThreadId]
const thread = state.allThreads[state.currentThreadId]
return thread
}
getFocusedMessageIdx() {
@@ -626,12 +602,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
getCurrentThreadStagingSelections = () => {
return this.getCurrentThread().state.stagingSelections
getCurrentThreadState = () => {
const currentThread = this.getCurrentThread()
return currentThread.state
}
setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => {
this._setCurrentThreadState({ stagingSelections })
setCurrentThreadState = (newState: Partial<ThreadType['state']>) => {
this._setCurrentThreadState(newState)
}
// gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected)
@@ -652,4 +629,3 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ChatMessage } from '../browser/chatThreadService.js'
import { ChatMessage } from './chatThreadService.js'
import { InternalToolInfo, ToolName } from './toolsService.js'
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
@@ -22,6 +22,11 @@ export const errorDetails = (fullError: Error | null): string | null => {
return null
}
export const getErrorMessage: (error: unknown) => string = (error) => {
if (error instanceof Error) return `${error.name}: ${error.message}`
return error + ''
}
export type LLMChatMessage = {
role: 'system' | 'user';
@@ -45,14 +50,14 @@ export type ToolCallType = {
}
export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id
export type OnText = (p: { fullText: string; fullReasoning: string }) => void
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string }) => void // id is tool_use_id
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
if (c.role === 'system' || c.role === 'user') {
if (c.role === 'user') {
return { role: c.role, content: c.content || '(empty message)' }
}
else if (c.role === 'assistant')
@@ -60,7 +65,7 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
else if (c.role === 'tool')
return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' }
else {
throw 1
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
@@ -6,7 +6,10 @@ import { createDecorator, IInstantiationService } from '../../../../platform/ins
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
import { IEditCodeService } from '../browser/editCodeService.js'
import { editToolDesc_toolDescription } from '../browser/prompt/prompts.js'
import { IVoidFileService } from './voidFileService.js'
// import { ITerminalToolService } from '../browser/terminalToolService.js'
// tool use for AI
@@ -29,6 +32,8 @@ const paginationHelper = {
} as const
export const voidTools = {
// --- context-gathering (read/search/list) ---
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
@@ -68,24 +73,50 @@ export const voidTools = {
required: ['query'],
},
// go_to_definition: {
// --- editing (create/delete) ---
// },
create_uri: {
name: 'create_uri',
description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
params: {
uri: { type: 'string', description: undefined },
},
required: ['uri'],
},
// go_to_usages:
delete_uri: {
name: 'delete_uri',
description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
params: {
uri: { type: 'string', description: undefined },
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
},
required: ['uri', 'params'],
},
// create_file: {
// name: 'create_file',
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`
// params: {
// uri: { type: 'string', description: undefined },
// }
// }
edit: { // APPLY TOOL
name: 'edit',
description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`,
params: {
uri: { type: 'string', description: undefined },
changeDescription: { type: 'string', description: editToolDesc_toolDescription }
},
required: ['uri', 'changeDescription'],
},
terminal_command: {
name: 'terminal_command',
description: `Executes a terminal command.`,
params: {
command: { type: 'string', description: 'The terminal command to execute.' }
},
required: ['command'],
},
// go_to_definition
// go_to_usages
// semantic_search: {
// description: 'Searches files semantically for the given string query.',
// // RAG
// },
} satisfies { [name: string]: InternalToolInfo }
export type ToolName = keyof typeof voidTools
@@ -104,12 +135,17 @@ export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T
export type ToolCallReturnType = {
'read_file': { uri: URI, fileContents: string, hasNextPage: boolean },
'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'pathname_search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean },
'search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean }
'create_file': {}
'pathname_search': { queryStr: string, uris: URI[], hasNextPage: boolean },
'search': { queryStr: string, uris: URI[], hasNextPage: boolean },
// ---
'edit': { uri: URI, changeDescription: string },
'create_uri': { uri: URI },
'delete_uri': { uri: URI },
'terminal_command': { command: string },
}
type DirectoryItem = {
uri: URI;
name: string;
isDirectory: boolean;
isSymbolicLink: boolean;
@@ -142,8 +178,9 @@ const computeDirectoryResult = async (
const children: DirectoryItem[] = listChildren.map(child => ({
name: child.name,
uri: child.resource,
isDirectory: child.isDirectory,
isSymbolicLink: child.isSymbolicLink || false
isSymbolicLink: child.isSymbolicLink
}));
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
@@ -196,32 +233,41 @@ const validateJSON = (s: string): { [s: string]: unknown } => {
return o
}
catch (e) {
throw new Error(`Tool parameter was not a valid JSON: "${s}".`)
throw new Error(`Tool parameter was not a string of a valid JSON: "${s}".`)
}
}
const validateQueryStr = (queryStr: unknown) => {
if (typeof queryStr !== 'string') throw new Error('Error calling tool: provided query must be a string.')
return queryStr
const validateStr = (argName: string, value: unknown) => {
if (typeof value !== 'string') throw new Error(`Error: ${argName} must be a string.`)
return value
}
// TODO!!!! check to make sure in workspace
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.')
if (typeof uriStr !== 'string') throw new Error('Error: provided uri must be a string.')
const uri = URI.file(uriStr)
return uri
}
const validatePageNum = (pageNumberUnknown: unknown) => {
const proposedPageNum = Number.parseInt(pageNumberUnknown + '')
const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1
const pageNumber = num < 1 ? 1 : num
return pageNumber
if (!pageNumberUnknown) return 1
const parsedInt = Number.parseInt(pageNumberUnknown + '')
if (!Number.isInteger(parsedInt)) throw new Error(`Page number was not an integer: "${pageNumberUnknown}".`)
if (parsedInt < 1) throw new Error(`Specified page number must be 1 or greater: "${pageNumberUnknown}".`)
return parsedInt
}
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
if (typeof paramsUnknown !== 'string') throw new Error('Error calling tool: provided params must be a string.')
const params = paramsUnknown
const isRecursive = params.includes('r')
return isRecursive
}
export interface IToolsService {
readonly _serviceBrand: undefined;
toolFns: ToolFns;
@@ -234,8 +280,8 @@ export class ToolsService implements IToolsService {
readonly _serviceBrand: undefined;
public toolFns: ToolFns
public toolResultToString: ToolResultToString
public toolFns: ToolFns;
public toolResultToString: ToolResultToString;
constructor(
@@ -244,15 +290,17 @@ export class ToolsService implements IToolsService {
@ISearchService searchService: ISearchService,
@IInstantiationService instantiationService: IInstantiationService,
@IVoidFileService voidFileService: IVoidFileService,
@IEditCodeService editCodeService: IEditCodeService,
// @ITerminalToolService private readonly terminalToolService: ITerminalToolService,
) {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.toolFns = {
read_file: async (s: string) => {
read_file: async (params: string) => {
console.log('read_file')
const o = validateJSON(s)
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
@@ -271,25 +319,21 @@ export class ToolsService implements IToolsService {
return { uri, fileContents, hasNextPage }
},
list_dir: async (s: string) => {
console.log('list_dir')
const o = validateJSON(s)
list_dir: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
const dirResult = await computeDirectoryResult(fileService, uri, pageNumber)
console.log('list_dir result:', dirResult)
return dirResult
},
pathname_search: async (s: string) => {
console.log('pathname_search')
const o = validateJSON(s)
pathname_search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateQueryStr(queryUnknown)
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
@@ -302,19 +346,13 @@ export class ToolsService implements IToolsService {
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
console.log('pathname_search result:', uris)
return { queryStr, uris, hasNextPage }
},
search: async (s: string) => {
console.log('search')
const o = validateJSON(s)
search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateQueryStr(queryUnknown)
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
@@ -327,17 +365,62 @@ export class ToolsService implements IToolsService {
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
console.log('search result:', uris)
return { queryStr, uris, hasNextPage }
},
// ---
create_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr } = o
const uri = validateURI(uriStr)
await fileService.createFile(uri)
return { uri }
},
delete_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, params: paramsStr } = o
const uri = validateURI(uriStr)
const isRecursive = validateRecursiveParamStr(paramsStr)
await fileService.del(uri, { recursive: isRecursive })
return { uri }
},
edit: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
// const applyId = editCodeService.startApplying({ uri, applyStr: changeDescription, from: 'ClickApply', type: 'rewrite' })
// // TODO!!!
// await // await apply done before moving on
return { uri, changeDescription }
},
terminal_command: async (s: string) => {
const o = validateJSON(s)
const { command: commandUnknown } = o
const command = validateStr('command', commandUnknown)
// TODO!!!!
// await // Await user confirmation and then command execution before resolving
return { command }
},
}
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
// given to the LLM after the call
this.toolResultToString = {
read_file: (result) => {
return nextPageStr(result.hasNextPage)
@@ -347,13 +430,25 @@ export class ToolsService implements IToolsService {
return dirTreeStr + nextPageStr(result.hasNextPage)
},
pathname_search: (result) => {
if (typeof result.uris === 'string') return result.uris
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
search: (result) => {
if (typeof result.uris === 'string') return result.uris
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
// ---
create_uri: (result) => {
return `URI ${result.uri.fsPath} successfully created.`
},
delete_uri: (result) => {
return `URI ${result.uri.fsPath} successfully deleted.`
},
edit: (result) => {
return `Change successfully made ${result.uri.fsPath} successfully deleted.`
},
terminal_command: (result) => {
return `Terminal command "${result.command}" successfully executed.`
},
}
@@ -28,7 +28,7 @@ type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
options?: { doNotApplyEffects?: true }
) => Promise<void>;
type SetGlobalSettingFn = <T extends GlobalSettingName, >(settingName: T, newVal: GlobalSettings[T]) => void;
type SetGlobalSettingFn = <T extends GlobalSettingName>(settingName: T, newVal: GlobalSettings[T]) => void;
export type ModelOption = { name: string, selection: ModelSelection }
@@ -49,6 +49,8 @@ export interface IVoidSettingsService {
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
readAndInitializeState: (providedState?: VoidSettingsState) => Promise<void>;
onDidChangeState: Event<void>;
setSettingOfProvider: SetSettingOfProviderFn;
@@ -168,6 +170,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: VoidSettingsState;
private readonly _resolver: () => void
waitForInitState: Promise<void> // await this if you need a valid state initially
constructor(
@@ -181,56 +185,57 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// at the start, we haven't read the partial config yet, but we need to set state to something
this.state = defaultState()
let resolver: () => void = () => { }
this.waitForInitState = new Promise((res, rej) => resolver = res)
this._resolver = resolver
// read and update the actual state immediately
this._readState().then(readS => {
this.readAndInitializeState()
}
// the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to)
const newSettingsOfProvider = {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
async readAndInitializeState(providedState?: VoidSettingsState) {
// If providedState is given, use it instead of reading from storage
const readS = providedState || await this._readState();
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
...{ xAI: defaultSettingsOfProvider.xAI },
// the stored data structure might be outdated, so we need to update it here
const newSettingsOfProvider = {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
...{ vLLM: defaultSettingsOfProvider.vLLM },
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
...{ xAI: defaultSettingsOfProvider.xAI },
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
...{ vLLM: defaultSettingsOfProvider.vLLM },
...readS.settingsOfProvider,
...readS.settingsOfProvider,
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
};
const newModelSelectionOfFeature = {
// A HACK BECAUSE WE ADDED FastApply
...{ 'Apply': null },
...readS.modelSelectionOfFeature,
}
const newModelSelectionOfFeature = {
// A HACK BECAUSE WE ADDED FastApply
...{ 'Apply': null },
...readS.modelSelectionOfFeature,
};
readS = {
...readS,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
}
const finalState = {
...readS,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
};
this.state = _validatedState(readS)
resolver()
this._onDidChangeState.fire()
})
this.state = _validatedState(finalState);
this._resolver();
this._onDidChangeState.fire();
}
@@ -3,16 +3,16 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import OpenAI, { ClientOptions } from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import { Ollama } from 'ollama';
import OpenAI, { ClientOptions } from 'openai';
import { Model as OpenAIModel } from 'openai/resources/models.js';
import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js';
import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js';
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js';
import { InternalToolInfo, isAToolName } from '../../common/toolsService.js';
import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
import { extractReasoningFromText } from '../../browser/helpers/extractCodeFromResult.js';
@@ -32,7 +32,7 @@ type ModelOptions = {
supportsReasoningOutput: false | {
// you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing)
// if it's open source, put the think tags here so we parse them out in e.g. ollama
openSourceThinkTags?: [string, string]
readonly openSourceThinkTags?: [string, string]
};
}
@@ -651,9 +651,9 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) {
if (modelName === modelName_)
onFinalMessage({ fullText: `Model ${modelName} does not support FIM.` })
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
else
onFinalMessage({ fullText: `Model ${modelName_} (${modelName}) does not support FIM.` })
onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null })
return
}
@@ -687,7 +687,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
supportsReasoningOutput,
supportsSystemMessage,
supportsTools,
maxOutputTokens,
// maxOutputTokens, right now we are ignoring this
} = getModelCapabilities(providerName, modelName_)
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, })
@@ -696,16 +696,19 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
const includeInPayload = supportsReasoningOutput ? modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.input?.includeInPayload || {} : {}
const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {}
const maxTokensObj = maxOutputTokens ? { max_tokens: maxOutputTokens } : {}
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, ...maxTokensObj }
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, }
const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {}
if (needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags)
onText = extractReasoningFromText(onText, supportsReasoningOutput.openSourceThinkTags)
let fullReasoning = ''
let fullText = ''
const manuallyParseReasoning = needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags
if (manuallyParseReasoning) {
onText = extractReasoningOnTextWrapper(onText, supportsReasoningOutput.openSourceThinkTags)
}
let fullReasoningSoFar = ''
let fullTextSoFar = ''
const toolCallOfIndex: ToolCallOfIndex = {}
openai.chat.completions
.create(options)
@@ -723,19 +726,31 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
}
// message
const newText = chunk.choices[0]?.delta?.content ?? ''
fullText += newText
fullTextSoFar += newText
// reasoning
let newReasoning = ''
if (nameOfReasoningFieldInDelta) {
// @ts-ignore
newReasoning = (chunk.choices[0]?.delta?.[nameOfReasoningFieldInDelta] || '') + ''
fullReasoning += newReasoning
fullReasoningSoFar += newReasoning
}
onText({ newText, fullText, newReasoning, fullReasoning })
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
// on final
const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex)
if (!fullTextSoFar && !fullReasoningSoFar && toolCalls.length === 0) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
if (manuallyParseReasoning) {
const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, supportsReasoningOutput.openSourceThinkTags)
onFinalMessage({ fullText, fullReasoning, toolCalls });
} else {
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls });
}
}
onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) });
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
@@ -797,7 +812,7 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[
}).filter(t => !!t)
}
const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
const {
// supportsReasoning: modelSupportsReasoning,
modelName,
@@ -822,7 +837,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM
})
// when receive text
stream.on('text', (newText, fullText) => {
onText({ newText, fullText, newReasoning: '', fullReasoning: '' })
onText({ fullText, fullReasoning: '' })
})
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (response) => {
@@ -63,22 +63,23 @@ export const sendLLMMessage = ({
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls }) => {
const onFinalMessage: OnFinalMessage = (params) => {
const { fullText, fullReasoning } = params
if (_didAbort) return
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText, toolCalls })
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_(params)
}
const onError: OnError = ({ message: error, fullError }) => {
const onError: OnError = ({ message: errorMessage, fullError }) => {
if (_didAbort) return
console.error('sendLLMMessage onError:', error)
console.error('sendLLMMessage onError:', errorMessage)
// handle failed to fetch errors, which give 0 information by design
if (error === 'TypeError: fetch failed')
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.`
if (errorMessage === 'TypeError: fetch failed')
errorMessage = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.`
captureLLMEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })
captureLLMEvent(`${loggingName} - Error`, { error: errorMessage })
onError_({ message: errorMessage, fullError })
}
const onAbort = () => {