mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
finish converting react to VS
This commit is contained in:
parent
8b9a9c7e2b
commit
6e2030d4d4
28 changed files with 2093 additions and 1341 deletions
326
package-lock.json
generated
326
package-lock.json
generated
|
|
@ -132,6 +132,7 @@
|
|||
"istanbul-lib-source-maps": "^4.0.1",
|
||||
"istanbul-reports": "^3.1.5",
|
||||
"lazy.js": "^0.4.2",
|
||||
"marked": "^15.0.0",
|
||||
"merge-options": "^1.0.1",
|
||||
"mime": "^1.4.1",
|
||||
"minimatch": "^3.0.4",
|
||||
|
|
@ -153,6 +154,7 @@
|
|||
"rcedit": "^1.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.1",
|
||||
"sinon": "^12.0.1",
|
||||
|
|
@ -988,6 +990,19 @@
|
|||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||
|
|
@ -2948,6 +2963,16 @@
|
|||
"@types/vinyl": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "2.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
|
||||
"integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
|
|
@ -3117,6 +3142,13 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/vinyl": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz",
|
||||
|
|
@ -5675,6 +5707,39 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
|
||||
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-legacy": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
|
||||
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-reference-invalid": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
|
||||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chardet": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||
|
|
@ -6156,6 +6221,17 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
|
||||
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-args": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",
|
||||
|
|
@ -8274,6 +8350,20 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fault": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
||||
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format": "^0.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
|
|
@ -8766,6 +8856,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
|
|
@ -11742,6 +11841,35 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-parse-selector": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
|
||||
"integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
|
||||
"integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^2.0.0",
|
||||
"comma-separated-tokens": "^1.0.0",
|
||||
"hast-util-parse-selector": "^2.0.0",
|
||||
"property-information": "^5.0.0",
|
||||
"space-separated-tokens": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
|
|
@ -11751,6 +11879,23 @@
|
|||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/highlightjs-vue": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/homedir-polyfill": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
|
||||
|
|
@ -12391,6 +12536,32 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
|
||||
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphanumerical": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
|
||||
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-alphabetical": "^1.0.0",
|
||||
"is-decimal": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||
|
|
@ -12588,6 +12759,17 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-decimal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
|
||||
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-deflate": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz",
|
||||
|
|
@ -12704,6 +12886,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-hexadecimal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
|
||||
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-interactive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||
|
|
@ -13934,6 +14127,21 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
||||
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
|
||||
|
|
@ -14040,6 +14248,19 @@
|
|||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.0.tgz",
|
||||
"integrity": "sha512-0mouKmBROJv/WSHJBPZZyYofUgawMChnD5je/g+aOBXsHDjb/IsnTQj7mnhQZu+qPJmRQ0ecX3mLGEUm3BgwYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/matchdep": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
|
||||
|
|
@ -15971,6 +16192,25 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-entities": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
|
||||
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities": "^1.0.0",
|
||||
"character-entities-legacy": "^1.0.0",
|
||||
"character-reference-invalid": "^1.0.0",
|
||||
"is-alphanumerical": "^1.0.0",
|
||||
"is-decimal": "^1.0.0",
|
||||
"is-hexadecimal": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-filepath": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
|
||||
|
|
@ -17278,6 +17518,16 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
|
|
@ -17302,6 +17552,20 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
|
||||
"integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
|
|
@ -17506,6 +17770,24 @@
|
|||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "15.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
|
||||
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"highlightjs-vue": "^1.0.0",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"refractor": "^3.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
|
@ -17706,6 +17988,39 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
|
||||
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hastscript": "^6.0.0",
|
||||
"parse-entities": "^2.0.0",
|
||||
"prismjs": "~1.27.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/prismjs": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
|
||||
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regex-not": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
|
||||
|
|
@ -19161,6 +19476,17 @@
|
|||
"deprecated": "See https://github.com/lydell/source-map-url#deprecated",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
|
||||
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/sparkles": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@
|
|||
"istanbul-lib-source-maps": "^4.0.1",
|
||||
"istanbul-reports": "^3.1.5",
|
||||
"lazy.js": "^0.4.2",
|
||||
"marked": "^15.0.0",
|
||||
"merge-options": "^1.0.1",
|
||||
"mime": "^1.4.1",
|
||||
"minimatch": "^3.0.4",
|
||||
|
|
@ -215,6 +216,7 @@
|
|||
"rcedit": "^1.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.1",
|
||||
"sinon": "^12.0.1",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,37 @@ import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
|
|||
import { IRange } from '../../../common/core/range.js';
|
||||
import { EditorOption } from '../../../common/config/editorOptions.js';
|
||||
|
||||
|
||||
|
||||
|
||||
// // an area that is currently being diffed
|
||||
// type DiffArea = {
|
||||
// diffareaid: number,
|
||||
// startLine: number,
|
||||
// endLine: number,
|
||||
// originalStartLine: number,
|
||||
// originalEndLine: number,
|
||||
// sweepIndex: number | null // null iff not sweeping
|
||||
// }
|
||||
|
||||
// // the return type of diff creator
|
||||
// type BaseDiff = {
|
||||
// type: 'edit' | 'insertion' | 'deletion';
|
||||
// // repr: string; // representation of the diff in text
|
||||
// originalRange: vscode.Range;
|
||||
// originalCode: string;
|
||||
// range: vscode.Range;
|
||||
// code: string;
|
||||
// }
|
||||
|
||||
// // each diff on the user's screen
|
||||
// type Diff = {
|
||||
// diffid: number,
|
||||
// lenses: vscode.CodeLens[],
|
||||
// } & BaseDiff
|
||||
|
||||
|
||||
|
||||
export interface IInlineDiffService {
|
||||
readonly _serviceBrand: undefined;
|
||||
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
|
||||
|
|
|
|||
|
|
@ -1,64 +1,66 @@
|
|||
const tailwindcss = require('tailwindcss')
|
||||
const autoprefixer = require('autoprefixer')
|
||||
const postcss = require('postcss')
|
||||
const fs = require('fs')
|
||||
// This is from the old repo
|
||||
|
||||
const convertTailwindToCSS = ({ from, to }) => {
|
||||
console.log('converting ', from, ' --> ', to)
|
||||
// const tailwindcss = require('tailwindcss')
|
||||
// const autoprefixer = require('autoprefixer')
|
||||
// const postcss = require('postcss')
|
||||
// const fs = require('fs')
|
||||
|
||||
const original_css_contents = fs.readFileSync(from, 'utf8')
|
||||
// const convertTailwindToCSS = ({ from, to }) => {
|
||||
// console.log('converting ', from, ' --> ', to)
|
||||
|
||||
return postcss([
|
||||
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
|
||||
autoprefixer,
|
||||
])
|
||||
.process(original_css_contents, { from, to })
|
||||
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
|
||||
.catch(error => {
|
||||
console.error('Error in build-css:', error)
|
||||
})
|
||||
}
|
||||
// const original_css_contents = fs.readFileSync(from, 'utf8')
|
||||
|
||||
// return postcss([
|
||||
// tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
|
||||
// autoprefixer,
|
||||
// ])
|
||||
// .process(original_css_contents, { from, to })
|
||||
// .then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
|
||||
// .catch(error => {
|
||||
// console.error('Error in build-css:', error)
|
||||
// })
|
||||
// }
|
||||
|
||||
|
||||
const esbuild = require('esbuild')
|
||||
// const esbuild = require('esbuild')
|
||||
|
||||
const convertTSXtoJS = async ({ from, to }) => {
|
||||
console.log('converting ', from, ' --> ', to)
|
||||
// const convertTSXtoJS = async ({ from, to }) => {
|
||||
// console.log('converting ', from, ' --> ', to)
|
||||
|
||||
return esbuild.build({
|
||||
entryPoints: [from],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
outfile: to,
|
||||
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
|
||||
platform: 'browser',
|
||||
external: ['vscode'],
|
||||
}).catch(() => process.exit(1));
|
||||
}
|
||||
// return esbuild.build({
|
||||
// entryPoints: [from],
|
||||
// bundle: true,
|
||||
// minify: true,
|
||||
// sourcemap: true,
|
||||
// outfile: to,
|
||||
// format: 'iife', // apparently iife is safe for browsers (safer than cjs)
|
||||
// platform: 'browser',
|
||||
// external: ['vscode'],
|
||||
// }).catch(() => process.exit(1));
|
||||
// }
|
||||
|
||||
(async () => {
|
||||
// convert tsx to js
|
||||
await convertTSXtoJS({
|
||||
from: 'src/webviews/sidebar/index.tsx',
|
||||
to: 'dist/webviews/sidebar/index.js',
|
||||
})
|
||||
// (async () => {
|
||||
// // convert tsx to js
|
||||
// await convertTSXtoJS({
|
||||
// from: 'src/webviews/sidebar/index.tsx',
|
||||
// to: 'dist/webviews/sidebar/index.js',
|
||||
// })
|
||||
|
||||
await convertTSXtoJS({
|
||||
from: 'src/webviews/ctrlk/index.tsx',
|
||||
to: 'dist/webviews/ctrlk/index.js',
|
||||
})
|
||||
// await convertTSXtoJS({
|
||||
// from: 'src/webviews/ctrlk/index.tsx',
|
||||
// to: 'dist/webviews/ctrlk/index.js',
|
||||
// })
|
||||
|
||||
await convertTSXtoJS({
|
||||
from: 'src/webviews/diffline/index.tsx',
|
||||
to: 'dist/webviews/diffline/index.js',
|
||||
})
|
||||
// await convertTSXtoJS({
|
||||
// from: 'src/webviews/diffline/index.tsx',
|
||||
// to: 'dist/webviews/diffline/index.js',
|
||||
// })
|
||||
|
||||
// convert tailwind to css
|
||||
await convertTailwindToCSS({
|
||||
from: 'src/webviews/styles.css',
|
||||
to: 'dist/webviews/styles.css',
|
||||
})
|
||||
// // convert tailwind to css
|
||||
// await convertTailwindToCSS({
|
||||
// from: 'src/webviews/styles.css',
|
||||
// to: 'dist/webviews/styles.css',
|
||||
// })
|
||||
|
||||
})()
|
||||
// })()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,184 +0,0 @@
|
|||
// import * as vscode from 'vscode';
|
||||
|
||||
// import { v4 as uuidv4 } from 'uuid'
|
||||
// import { AbortRef } from '../common/sendLLMMessage';
|
||||
// import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
|
||||
// import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
|
||||
// import { DiffProvider } from '../../DiffProvider';
|
||||
// import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
|
||||
// import { SidebarWebviewProvider } from '../sidebar/SidebarWebviewProvider';
|
||||
// import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
|
||||
|
||||
// const roundRangeToLines = (selection: vscode.Selection) => {
|
||||
// let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
|
||||
// return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER)
|
||||
// }
|
||||
|
||||
// const getSelection = (editor: vscode.TextEditor) => {
|
||||
// // get the range of the selection and the file the user is in
|
||||
// const selectionRange = roundRangeToLines(editor.selection);
|
||||
// const selectionStr = editor.document.getText(selectionRange).trim();
|
||||
// const filePath = editor.document.uri;
|
||||
// return { selectionStr, filePath }
|
||||
// }
|
||||
|
||||
// export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// // 1. Mount the chat sidebar
|
||||
// const sidebarWebviewProvider = new SidebarWebviewProvider(context);
|
||||
// context.subscriptions.push(
|
||||
// vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
|
||||
// );
|
||||
|
||||
// // 1.5
|
||||
// const ctrlKWebviewProvider = new CtrlKWebviewProvider(context)
|
||||
|
||||
|
||||
// // 2. ctrl+l
|
||||
// context.subscriptions.push(
|
||||
// vscode.commands.registerCommand('void.ctrl+l', () => {
|
||||
// const editor = vscode.window.activeTextEditor
|
||||
// if (!editor) return
|
||||
|
||||
// // show the sidebar
|
||||
// vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
|
||||
// // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
|
||||
|
||||
// const { selectionStr, filePath } = getSelection(editor)
|
||||
|
||||
// // send message to the webview (Sidebar.tsx)
|
||||
// sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
|
||||
// })
|
||||
// );
|
||||
|
||||
// // 2.5: ctrl+k
|
||||
// context.subscriptions.push(
|
||||
// vscode.commands.registerCommand('void.ctrl+k', () => {
|
||||
// console.log('CTRLK PRESSED')
|
||||
// const editor = vscode.window.activeTextEditor
|
||||
// if (!editor) return
|
||||
|
||||
// const { selectionStr, filePath } = getSelection(editor)
|
||||
|
||||
// // send message to the webview (Sidebar.tsx)
|
||||
// // ctrlKWebviewProvider.onPressCtrlK()
|
||||
// // sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
|
||||
// })
|
||||
// );
|
||||
|
||||
// // 3. Show an approve/reject codelens above each change
|
||||
// const diffProvider = new DiffProvider(context);
|
||||
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
|
||||
|
||||
// // 4. Add approve/reject commands
|
||||
// context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
|
||||
// diffProvider.acceptDiff(params)
|
||||
// }));
|
||||
// context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
|
||||
// diffProvider.rejectDiff(params)
|
||||
// }));
|
||||
|
||||
// // 5. Receive messages from sidebar
|
||||
// sidebarWebviewProvider.webview.then(
|
||||
// webview => {
|
||||
|
||||
// // top navigation bar commands
|
||||
// context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
|
||||
// webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
|
||||
// }))
|
||||
// context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
|
||||
// webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
|
||||
// }))
|
||||
// context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
|
||||
// webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
|
||||
// }));
|
||||
|
||||
// // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
|
||||
// webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
|
||||
|
||||
// const abortApplyRef: AbortRef = { current: null }
|
||||
|
||||
// if (m.type === 'requestFiles') {
|
||||
|
||||
// // get contents of all file paths
|
||||
// const files = await Promise.all(
|
||||
// m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) }))
|
||||
// )
|
||||
|
||||
// // send contents to webview
|
||||
// webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
|
||||
|
||||
// }
|
||||
// else if (m.type === 'applyChanges') {
|
||||
|
||||
// const editor = vscode.window.activeTextEditor
|
||||
// if (!editor) {
|
||||
// vscode.window.showInformationMessage('No active editor!')
|
||||
// return
|
||||
// }
|
||||
// // create an area to show diffs
|
||||
// const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
|
||||
// startLine: 0, // in ctrl+L the start and end lines are the full document
|
||||
// endLine: editor.document.lineCount,
|
||||
// originalStartLine: 0,
|
||||
// originalEndLine: editor.document.lineCount,
|
||||
// sweepIndex: null,
|
||||
// }
|
||||
// const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
|
||||
|
||||
// const docUri = editor.document.uri
|
||||
// const fileStr = await readFileContentOfUri(docUri)
|
||||
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
// await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffArea, abortRef: abortApplyRef })
|
||||
// }
|
||||
// else if (m.type === 'getPartialVoidConfig') {
|
||||
// const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
|
||||
// webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
|
||||
// }
|
||||
// else if (m.type === 'persistPartialVoidConfig') {
|
||||
// const partialVoidConfig = m.partialVoidConfig
|
||||
// context.globalState.update('partialVoidConfig', partialVoidConfig)
|
||||
// }
|
||||
// else if (m.type === 'getAllThreads') {
|
||||
// const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||
// webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
|
||||
// }
|
||||
// else if (m.type === 'persistThread') {
|
||||
// const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
|
||||
// const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
|
||||
// context.workspaceState.update('allThreads', updatedThreads)
|
||||
// }
|
||||
// else if (m.type === 'getDeviceId') {
|
||||
// let deviceId = context.globalState.get('void_deviceid')
|
||||
// if (!deviceId || typeof deviceId !== 'string') {
|
||||
// deviceId = uuidv4()
|
||||
// context.globalState.update('void_deviceid', deviceId)
|
||||
// }
|
||||
// webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
|
||||
// }
|
||||
// else {
|
||||
// console.error('unrecognized command', m)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// )
|
||||
|
||||
|
||||
|
||||
|
||||
// // Gets called when user presses ctrl + k (mounts ctrl+k-style codelens)
|
||||
// // TODO need to build this
|
||||
// // const ctrlKCodeLensProvider = new CtrlKCodeLensProvider();
|
||||
// // context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider));
|
||||
// // context.subscriptions.push(
|
||||
// // vscode.commands.registerCommand('void.ctrl+k', () => {
|
||||
// // const editor = vscode.window.activeTextEditor;
|
||||
// // if (!editor)
|
||||
// // return
|
||||
// // ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection);
|
||||
// // // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses
|
||||
// // })
|
||||
// // )
|
||||
|
||||
// }
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
// import Anthropic from '@anthropic-ai/sdk';
|
||||
// import OpenAI from 'openai';
|
||||
// import { Ollama } from 'ollama/browser'
|
||||
// import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
// // import { VoidConfig } from '../webviews/common/contextForConfig'
|
||||
// // import { captureEvent } from '../webviews/common/posthog';
|
||||
// // import { ChatMessage } from './shared_types';
|
||||
|
||||
// type VoidConfig = any
|
||||
|
||||
// export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
// export type OnText = (newText: string, fullText: string) => void
|
||||
|
||||
// export type OnFinalMessage = (input: string) => void
|
||||
|
||||
// export type LLMMessageAnthropic = {
|
||||
// role: 'user' | 'assistant';
|
||||
// content: string;
|
||||
// }
|
||||
|
||||
// export type LLMMessage = {
|
||||
// role: 'system' | 'user' | 'assistant';
|
||||
// content: string;
|
||||
// }
|
||||
|
||||
// type SendLLMMessageFnTypeInternal = (params: {
|
||||
// messages: LLMMessage[];
|
||||
// onText: OnText;
|
||||
// onFinalMessage: OnFinalMessage;
|
||||
// onError: (error: string) => void;
|
||||
// voidConfig: VoidConfig;
|
||||
|
||||
// _setAborter: (aborter: () => void) => void;
|
||||
// }) => void
|
||||
|
||||
// type SendLLMMessageFnTypeExternal = (params: {
|
||||
// messages: LLMMessage[];
|
||||
// onText: OnText;
|
||||
// onFinalMessage: (fullText: string) => void;
|
||||
// onError: (error: string) => void;
|
||||
// voidConfig: VoidConfig | null;
|
||||
// abortRef: AbortRef;
|
||||
|
||||
// logging: {
|
||||
// loggingName: string,
|
||||
// };
|
||||
// }) => void
|
||||
|
||||
// const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
// // Anthropic
|
||||
// const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
// const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
|
||||
|
||||
// // find system messages and concatenate them
|
||||
// const systemMessage = messages
|
||||
// .filter(msg => msg.role === 'system')
|
||||
// .map(msg => msg.content)
|
||||
// .join('\n');
|
||||
|
||||
// // remove system messages for Anthropic
|
||||
// const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
// const stream = anthropic.messages.stream({
|
||||
// system: systemMessage,
|
||||
// messages: anthropicMessages,
|
||||
// model: voidConfig.anthropic.model,
|
||||
// max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
|
||||
// });
|
||||
|
||||
|
||||
// // when receive text
|
||||
// stream.on('text', (newText, fullText) => {
|
||||
// onText(newText, fullText)
|
||||
// })
|
||||
|
||||
// // when we get the final message on this stream (or when error/fail)
|
||||
// stream.on('finalMessage', (claude_response) => {
|
||||
// // stringify the response's content
|
||||
// const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
// onFinalMessage(content)
|
||||
// })
|
||||
|
||||
// stream.on('error', (error) => {
|
||||
// // the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
// if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
// onError('Invalid API key.')
|
||||
// }
|
||||
// else {
|
||||
// onError(error.message)
|
||||
// }
|
||||
// })
|
||||
|
||||
// // TODO need to test this to make sure it works, it might throw an error
|
||||
// _setAborter(() => stream.controller.abort())
|
||||
|
||||
// };
|
||||
|
||||
// // Gemini
|
||||
// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
|
||||
// const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
|
||||
|
||||
// // remove system messages that get sent to Gemini
|
||||
// // str of all system messages
|
||||
// const systemMessage = messages
|
||||
// .filter(msg => msg.role === 'system')
|
||||
// .map(msg => msg.content)
|
||||
// .join('\n');
|
||||
|
||||
// // Convert messages to Gemini format
|
||||
// const geminiMessages: Content[] = messages
|
||||
// .filter(msg => msg.role !== 'system')
|
||||
// .map((msg, i) => ({
|
||||
// parts: [{ text: msg.content }],
|
||||
// role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
// }))
|
||||
|
||||
// model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
// .then(async response => {
|
||||
// _setAborter(() => response.stream.return(fullText))
|
||||
|
||||
// for await (const chunk of response.stream) {
|
||||
// const newText = chunk.text();
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// if (error instanceof GoogleGenerativeAIFetchError) {
|
||||
// if (error.status === 400) {
|
||||
// onError('Invalid API key.');
|
||||
// }
|
||||
// else {
|
||||
// onError(`${error.name}:\n${error.message}`);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// onError(error);
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// // OpenAI, OpenRouter, OpenAICompatible
|
||||
// const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// let openai: OpenAI
|
||||
// let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
// const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
|
||||
|
||||
// if (voidConfig.default.whichApi === 'openAI') {
|
||||
// openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
// options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
// }
|
||||
// else if (voidConfig.default.whichApi === 'openRouter') {
|
||||
// openai = new OpenAI({
|
||||
// baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
|
||||
// defaultHeaders: {
|
||||
// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
// },
|
||||
// });
|
||||
// options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
// }
|
||||
// else if (voidConfig.default.whichApi === 'openAICompatible') {
|
||||
// openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
|
||||
// options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
// }
|
||||
// else {
|
||||
// console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
|
||||
// throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
|
||||
// }
|
||||
|
||||
// openai.chat.completions
|
||||
// .create(options)
|
||||
// .then(async response => {
|
||||
// _setAborter(() => response.controller.abort())
|
||||
// // when receive text
|
||||
// for await (const chunk of response) {
|
||||
// const newText = chunk.choices[0]?.delta?.content || '';
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
// })
|
||||
// // when error/fail - this catches errors of both .create() and .then(for await)
|
||||
// .catch(error => {
|
||||
// if (error instanceof OpenAI.APIError) {
|
||||
// if (error.status === 401) {
|
||||
// onError('Invalid API key.');
|
||||
// }
|
||||
// else {
|
||||
// onError(`${error.name}:\n${error.message}`);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// onError(error);
|
||||
// }
|
||||
// })
|
||||
|
||||
// };
|
||||
|
||||
// // Ollama
|
||||
// export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
// ollama.chat({
|
||||
// model: voidConfig.ollama.model,
|
||||
// messages: messages,
|
||||
// stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
|
||||
// })
|
||||
// .then(async stream => {
|
||||
// _setAborter(() => stream.abort())
|
||||
// // iterate through the stream
|
||||
// for await (const chunk of stream) {
|
||||
// const newText = chunk.message.content;
|
||||
// fullText += newText;
|
||||
// onText(newText, fullText);
|
||||
// }
|
||||
// onFinalMessage(fullText);
|
||||
|
||||
// })
|
||||
// // when error/fail
|
||||
// .catch(error => {
|
||||
// onError(error)
|
||||
// })
|
||||
|
||||
// };
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
// // https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
// const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// fetch('https://api.greptile.com/v2/query', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
|
||||
// 'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
|
||||
// 'Content-Type': `application/json`,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// messages,
|
||||
// stream: true,
|
||||
// repositories: [voidConfig.greptile.repoinfo],
|
||||
// }),
|
||||
// })
|
||||
// // this is {message}\n{message}\n{message}...\n
|
||||
// .then(async response => {
|
||||
// const text = await response.text()
|
||||
// console.log('got greptile', text)
|
||||
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
// })
|
||||
// // TODO make this actually stream, right now it just sends one message at the end
|
||||
// // TODO add _setAborter() when add streaming
|
||||
// .then(async responseArr => {
|
||||
|
||||
// for (const response of responseArr) {
|
||||
// const type: string = response['type']
|
||||
// const message = response['message']
|
||||
|
||||
// // when receive text
|
||||
// if (type === 'message') {
|
||||
// fullText += message
|
||||
// onText(message, fullText)
|
||||
// }
|
||||
// else if (type === 'sources') {
|
||||
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
|
||||
// fullText += filepath
|
||||
// onText(filepath, fullText)
|
||||
// }
|
||||
// // type: 'status' with an empty 'message' means last message
|
||||
// else if (type === 'status') {
|
||||
// if (!message) {
|
||||
// onFinalMessage(fullText)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// })
|
||||
// .catch(e => {
|
||||
// onError(e)
|
||||
// });
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
|
||||
// messages,
|
||||
// onText: onText_,
|
||||
// onFinalMessage: onFinalMessage_,
|
||||
// onError: onError_,
|
||||
// abortRef: abortRef_,
|
||||
// voidConfig,
|
||||
// logging: { loggingName }
|
||||
// }) => {
|
||||
// if (!voidConfig) return;
|
||||
|
||||
// // trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
// messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// // only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
// const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
// // captureEvent(eventId, {
|
||||
// // whichApi: voidConfig.default['whichApi'],
|
||||
// // numMessages: messages?.length,
|
||||
// // messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
// // version: '2024-11-02',
|
||||
// // ...extras,
|
||||
// // })
|
||||
// }
|
||||
// const submit_time = new Date()
|
||||
|
||||
// let _fullTextSoFar = ''
|
||||
// let _aborter: (() => void) | null = null
|
||||
// let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
// let _didAbort = false
|
||||
|
||||
// const onText = (newText: string, fullText: string) => {
|
||||
// if (_didAbort) return
|
||||
// onText_(newText, fullText)
|
||||
// _fullTextSoFar = fullText
|
||||
// }
|
||||
|
||||
// const onFinalMessage = (fullText: string) => {
|
||||
// if (_didAbort) return
|
||||
// captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
// onFinalMessage_(fullText)
|
||||
// }
|
||||
|
||||
// const onError = (error: string) => {
|
||||
// if (_didAbort) return
|
||||
// captureChatEvent(`${loggingName} - Error`, { error })
|
||||
// onError_(error)
|
||||
// }
|
||||
|
||||
// const onAbort = () => {
|
||||
// captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
// _aborter?.()
|
||||
// _didAbort = true
|
||||
// }
|
||||
// abortRef_.current = onAbort
|
||||
|
||||
// captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
// switch (voidConfig.default.whichApi) {
|
||||
// case 'anthropic':
|
||||
// sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
// break;
|
||||
// case 'openAI':
|
||||
// case 'openRouter':
|
||||
// case 'openAICompatible':
|
||||
// sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
// break;
|
||||
// case 'gemini':
|
||||
// sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
// break;
|
||||
// case 'ollama':
|
||||
// sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
// break;
|
||||
// case 'greptile':
|
||||
// sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
// break;
|
||||
// default:
|
||||
// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
// break;
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/* all the styles are shared right now between all webviews */
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply cursor-pointer transition-colors;
|
||||
|
||||
&.btn-primary {
|
||||
@apply bg-vscode-button-bg text-vscode-button-fg;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-sm {
|
||||
@apply px-3 py-1 text-sm;
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-secondary-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
/* add transparency when disabled */
|
||||
&:disabled {
|
||||
@apply opacity-75 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border focus:outline-vscode-focus-border;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border;
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
module.exports = {
|
||||
content: ["./src/webviews/**/*.{html,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
vscode: {
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"button-fg": "var(--vscode-button-foreground)",
|
||||
"button-bg": "var(--vscode-button-background)",
|
||||
"button-hoverBg": "var(--vscode-button-hoverBackground)",
|
||||
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
|
||||
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
|
||||
"button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
"dropdown-bg": "var(--vscode-settings-dropdownBackground)",
|
||||
"dropdown-foreground": "var(--vscode-settings-dropdownForeground)",
|
||||
"dropdown-border": "var(--vscode-settings-dropdownBorder)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@ Run `node build.js` to compile the React into `out/`.
|
|||
|
||||
A couple things to remember:
|
||||
|
||||
- Make sure to add .js at the end of any external imports used in here.
|
||||
- Make sure to add .js at the end of any external imports used in here, e.g. ../../../../../my_file.js. If you don't do this, you will get untraceable errors.
|
||||
|
||||
- src/ needs to be shallow so the detection of externals works properly (see tsup.config.js).
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
|
||||
|
||||
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
const customStyle = {
|
||||
...atomOneDarkReasonable,
|
||||
'code[class*="language-"]': {
|
||||
...atomOneDarkReasonable['code[class*="language-"]'],
|
||||
background: "none",
|
||||
},
|
||||
}
|
||||
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
|
||||
|
||||
{!toolbar ? null : (
|
||||
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
language={language ?? 'plaintext'} // TODO must auto detect language
|
||||
style={customStyle}
|
||||
className={"rounded-sm"}
|
||||
>
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
import React, { JSX, useCallback, useEffect, useState } from "react"
|
||||
import { marked, MarkedToken, Token, TokensList } from "marked"
|
||||
import { BlockCode } from "./BlockCode.js"
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = "Copy",
|
||||
Copied = "Copied!",
|
||||
Error = "Could not copy",
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy)
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}
|
||||
}, [copyButtonState])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Copied)
|
||||
},
|
||||
() => {
|
||||
setCopyButtonState(CopyButtonState.Error)
|
||||
}
|
||||
)
|
||||
}, [text])
|
||||
|
||||
return <>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
getVSCodeAPI().postMessage({ type: "applyChanges", diffRepr: text })
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
|
||||
if (t.type === "space") {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
return <BlockCode
|
||||
text={t.text}
|
||||
language={t.lang}
|
||||
buttonsOnHover={<CodeButtonsOnHover diffRepr={t.text} />}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
return <HeadingTag>{t.text}</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
|
||||
{cell.raw}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td
|
||||
key={cellIndex}
|
||||
style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
>
|
||||
{cell.raw}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
const ListTag = t.ordered ? "ol" : "ul"
|
||||
return (
|
||||
<ListTag
|
||||
start={t.start ? t.start : undefined}
|
||||
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
<MarkdownRender string={item.text} nested={true} />
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} />
|
||||
))}
|
||||
</>
|
||||
if (nested)
|
||||
return contents
|
||||
return <p>{contents}</p>
|
||||
}
|
||||
|
||||
// don't actually render <html> tags, just render strings of them
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<pre>
|
||||
{`<html>`}
|
||||
{t.raw}
|
||||
{`</html>`}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
return <></> // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
return (
|
||||
<a href={t.href} title={t.title ?? undefined}>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong>{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em>{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return (
|
||||
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
|
||||
{t.text}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
return <br />
|
||||
}
|
||||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
return <del>{t.text}</del>
|
||||
}
|
||||
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden">
|
||||
<span className="text-xs text-orange-500">Unknown type:</span>
|
||||
{t.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
|
||||
import { ChatFile, ChatCodeSelection } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
export const filesStr = (fullFiles: ChatFile[]) => {
|
||||
return fullFiles.map(({ filepath, content }) =>
|
||||
`
|
||||
${filepath.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\``).join('\n')
|
||||
}
|
||||
|
||||
|
||||
export const userInstructionsStr = (instructions: string, files: ChatFile[], selection: ChatCodeSelection | null) => {
|
||||
let str = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
str += filesStr(files);
|
||||
}
|
||||
|
||||
if (selection) {
|
||||
str += `
|
||||
I am currently selecting this code:
|
||||
\t\`\`\`${selection.selectionStr}\`\`\`
|
||||
`;
|
||||
}
|
||||
|
||||
if (files.length > 0 && selection) {
|
||||
str += `
|
||||
Please edit the selected code or the entire file following these instructions:
|
||||
`;
|
||||
} else if (files.length > 0) {
|
||||
str += `
|
||||
Please edit the file following these instructions:
|
||||
`;
|
||||
} else if (selection) {
|
||||
str += `
|
||||
Please edit the selected code following these instructions:
|
||||
`;
|
||||
}
|
||||
|
||||
str += `
|
||||
\t${instructions}
|
||||
`;
|
||||
if (files.length > 0) {
|
||||
str += `
|
||||
\tIf you make a change, rewrite the entire file.
|
||||
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
const generateDiffInstructions = `
|
||||
export const generateDiffInstructions = `
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
|
@ -160,7 +160,7 @@ We should change all the buttons like the one selected into a div component. Her
|
|||
`;
|
||||
|
||||
|
||||
const searchDiffChunkInstructions = `
|
||||
export const searchDiffChunkInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
|
||||
|
||||
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
|
||||
|
|
@ -269,7 +269,7 @@ OUTPUT
|
|||
`
|
||||
|
||||
|
||||
const writeFileWithDiffInstructions = `
|
||||
export const writeFileWithDiffInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`.
|
||||
|
|
@ -398,9 +398,3 @@ export default Sidebar;\`\`\`
|
|||
`
|
||||
|
||||
|
||||
|
||||
export {
|
||||
generateDiffInstructions,
|
||||
searchDiffChunkInstructions,
|
||||
writeFileWithDiffInstructions,
|
||||
};
|
||||
|
|
@ -2,19 +2,15 @@ import React, { useEffect, useState } from 'react'
|
|||
import { mountFnGenerator } from '../util/mountFnGenerator'
|
||||
|
||||
import { SidebarSettings } from './SidebarSettings.js';
|
||||
import { useServices } from '../util/contextForServices.js';
|
||||
import { IVoidSidebarStateService, VoidSidebarState } from '../../../registerSidebar.js';
|
||||
import { useSidebarState } from '../util/contextForServices.js';
|
||||
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
// import { SidebarChat } from './SidebarChat.js';
|
||||
|
||||
import '../styles.css'
|
||||
|
||||
const Sidebar = () => {
|
||||
// state should come from sidebarStateService
|
||||
const { sidebarStateService } = useServices()
|
||||
const [sidebarState, setSideBarState] = useState<VoidSidebarState>(sidebarStateService.state)
|
||||
const [sidebarState, sidebarStateService] = useSidebarState()
|
||||
const { isHistoryOpen, currentTab: tab } = sidebarState
|
||||
useEffect(() => { sidebarStateService.onDidChangeState(() => setSideBarState(sidebarStateService.state)) }, [sidebarStateService])
|
||||
|
||||
return <div className='@@void-scope'>
|
||||
<div className={`flex flex-col h-screen w-full`}>
|
||||
|
|
|
|||
|
|
@ -1,355 +1,339 @@
|
|||
// import React, { FormEvent, useCallback, useRef, useState } from "react";
|
||||
|
||||
|
||||
// sidebarStateService.onDidFocusChat(() => {})
|
||||
// sidebarStateService.onDidBlurChat(() => {})
|
||||
|
||||
|
||||
|
||||
// import MarkdownRender from "../../sidebar/markdown/!MarkdownRender";
|
||||
// import BlockCode from "../../sidebar/markdown/!BlockCode";
|
||||
// import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation';
|
||||
|
||||
|
||||
// const filesStr = (fullFiles: File[]) => {
|
||||
// return fullFiles.map(({ filepath, content }) =>
|
||||
// `
|
||||
// ${filepath.fsPath}
|
||||
// \`\`\`
|
||||
// ${content}
|
||||
// \`\`\``).join('\n')
|
||||
// }
|
||||
|
||||
// const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
|
||||
// let str = '';
|
||||
|
||||
// if (files.length > 0) {
|
||||
// str += filesStr(files);
|
||||
// }
|
||||
|
||||
// if (selection) {
|
||||
// str += `
|
||||
// I am currently selecting this code:
|
||||
// \t\`\`\`${selection.selectionStr}\`\`\`
|
||||
// `;
|
||||
// }
|
||||
|
||||
// if (files.length > 0 && selection) {
|
||||
// str += `
|
||||
// Please edit the selected code or the entire file following these instructions:
|
||||
// `;
|
||||
// } else if (files.length > 0) {
|
||||
// str += `
|
||||
// Please edit the file following these instructions:
|
||||
// `;
|
||||
// } else if (selection) {
|
||||
// str += `
|
||||
// Please edit the selected code following these instructions:
|
||||
// `;
|
||||
// }
|
||||
|
||||
// str += `
|
||||
// \t${instructions}
|
||||
// `;
|
||||
// if (files.length > 0) {
|
||||
// str += `
|
||||
// \tIf you make a change, rewrite the entire file.
|
||||
// `; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
|
||||
// }
|
||||
// return str;
|
||||
// };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// const getBasename = (pathStr: string) => {
|
||||
// // "unixify" path
|
||||
// pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
|
||||
// const parts = pathStr.split("/") // split on /
|
||||
// return parts[parts.length - 1]
|
||||
// }
|
||||
|
||||
// export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
|
||||
// return (
|
||||
// files.length !== 0 && (
|
||||
// <div className="flex flex-wrap -mx-1 -mb-1">
|
||||
// {files.map((filename, i) => (
|
||||
// <button
|
||||
// key={filename.path}
|
||||
// disabled={!setFiles}
|
||||
// className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
|
||||
// type="button"
|
||||
// onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
|
||||
// >
|
||||
// <span>{getBasename(filename.fsPath)}</span>
|
||||
|
||||
// {/* X button */}
|
||||
// {!!setFiles && <span className="">
|
||||
// <svg
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// fill="none"
|
||||
// viewBox="0 0 24 24"
|
||||
// stroke="currentColor"
|
||||
// className="size-4"
|
||||
// >
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// d="M6 18 18 6M6 6l12 12"
|
||||
// />
|
||||
// </svg>
|
||||
// </span>}
|
||||
// </button>
|
||||
// ))}
|
||||
// </div>
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
|
||||
|
||||
// const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
|
||||
// const role = chatMessage.role
|
||||
// const children = chatMessage.displayContent
|
||||
|
||||
// if (!children)
|
||||
// return null
|
||||
|
||||
// let chatbubbleContents: React.ReactNode
|
||||
|
||||
// if (role === 'user') {
|
||||
// chatbubbleContents = <>
|
||||
// <SelectedFiles files={chatMessage.files} setFiles={null} />
|
||||
// {chatMessage.selection?.selectionStr && <BlockCode
|
||||
// text={chatMessage.selection.selectionStr}
|
||||
// buttonsOnHover={null}
|
||||
// />}
|
||||
// {children}
|
||||
// </>
|
||||
// }
|
||||
// else if (role === 'assistant') {
|
||||
// chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
|
||||
// }
|
||||
|
||||
// return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||
// <div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
||||
// {chatbubbleContents}
|
||||
// </div>
|
||||
// </div>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HTMLTextAreaElement> }) => {
|
||||
|
||||
// // // if they pressed the + to add a new chat
|
||||
// // useOnVSCodeMessage('startNewThread', (m) => {
|
||||
// // const allThreads = getAllThreads()
|
||||
// // // find a thread with 0 messages and switch to it
|
||||
// // for (let threadId in allThreads) {
|
||||
// // if (allThreads[threadId].messages.length === 0) {
|
||||
// // switchToThread(threadId)
|
||||
// // return
|
||||
// // }
|
||||
// // }
|
||||
// // // start a new thread
|
||||
// // startNewThread()
|
||||
// // })
|
||||
|
||||
// // // if user pressed ctrl+l, add their selection to the sidebar
|
||||
// // useOnVSCodeMessage('ctrl+l', (m) => {
|
||||
// // setSelection(m.selection)
|
||||
// // const filepath = m.selection.filePath
|
||||
|
||||
// // // add current file to the context if it's not already in the files array
|
||||
// // if (!files.find(f => f.fsPath === filepath.fsPath))
|
||||
// // setFiles(files => [...files, filepath])
|
||||
// // })
|
||||
|
||||
// // state of current message
|
||||
// const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
|
||||
// const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
|
||||
// const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
|
||||
// // state of chat
|
||||
// const [messageStream, setMessageStream] = useState('')
|
||||
// const [isLoading, setIsLoading] = useState(false)
|
||||
// const abortFnRef = useRef<(() => void) | null>(null)
|
||||
|
||||
// const [latestError, setLatestError] = useState('')
|
||||
|
||||
// // higher level state
|
||||
// const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
|
||||
|
||||
// const { voidConfig } = useVoidConfig()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// const isDisabled = !instructions
|
||||
|
||||
// const formRef = useRef<HTMLFormElement | null>(null)
|
||||
// const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
// e.preventDefault()
|
||||
// if (isDisabled) return
|
||||
// if (isLoading) return
|
||||
|
||||
// setIsLoading(true)
|
||||
// setInstructions('');
|
||||
// formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
|
||||
// setSelection(null)
|
||||
// setFiles([])
|
||||
// setLatestError('')
|
||||
|
||||
// // request file content from vscode and await response
|
||||
// getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
||||
// const relevantFiles = await awaitVSCodeResponse('files')
|
||||
|
||||
// // add system message to chat history
|
||||
// const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
|
||||
// addMessageToHistory(systemPromptElt)
|
||||
|
||||
// const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
|
||||
// const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
|
||||
// addMessageToHistory(newHistoryElt)
|
||||
|
||||
// // send message to LLM
|
||||
// sendLLMMessage({
|
||||
// logging: { loggingName: 'Chat' },
|
||||
// messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
|
||||
// onText: (newText, fullText) => setMessageStream(fullText),
|
||||
// onFinalMessage: (content) => {
|
||||
|
||||
// // add assistant's message to chat history, and clear selection
|
||||
// const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
|
||||
// addMessageToHistory(newHistoryElt)
|
||||
// setMessageStream('')
|
||||
// setIsLoading(false)
|
||||
// },
|
||||
// onError: (error) => {
|
||||
// // add assistant's message to chat history, and clear selection
|
||||
// let content = messageStream; // just use the current content
|
||||
// const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||
// addMessageToHistory(newHistoryElt)
|
||||
// setMessageStream('')
|
||||
// setIsLoading(false)
|
||||
|
||||
// setLatestError(error)
|
||||
// },
|
||||
// voidConfig,
|
||||
// abortRef: abortFnRef,
|
||||
// })
|
||||
|
||||
|
||||
// }
|
||||
|
||||
// const onAbort = useCallback(() => {
|
||||
// // abort claude
|
||||
// abortFnRef.current?.()
|
||||
|
||||
// // if messageStream was not empty, add it to the history
|
||||
// const llmContent = messageStream || '(null)'
|
||||
// const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
|
||||
// addMessageToHistory(newHistoryElt)
|
||||
|
||||
// setMessageStream('')
|
||||
// setIsLoading(false)
|
||||
|
||||
// }, [messageStream, addMessageToHistory])
|
||||
|
||||
|
||||
// return <>
|
||||
// <div className="overflow-x-hidden space-y-4">
|
||||
// {/* previous messages */}
|
||||
// {getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) =>
|
||||
// <ChatBubble key={i} chatMessage={message} />
|
||||
// )}
|
||||
// {/* message stream */}
|
||||
// <ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
||||
// </div>
|
||||
// {/* chatbar */}
|
||||
// <div className="shrink-0 py-4">
|
||||
// {/* selection */}
|
||||
// <div className="text-left">
|
||||
// <div className="relative">
|
||||
// <div className="input">
|
||||
// {/* selection */}
|
||||
// {(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
||||
// {/* selected files */}
|
||||
// <SelectedFiles files={files} setFiles={setFiles} />
|
||||
// {/* selected code */}
|
||||
// {!!selection?.selectionStr && (
|
||||
// <BlockCode text={selection.selectionStr}
|
||||
// buttonsOnHover={(
|
||||
// <button
|
||||
// onClick={() => setSelection(null)}
|
||||
// className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
// >
|
||||
// Remove
|
||||
// </button>
|
||||
// )} />
|
||||
// )}
|
||||
// </div>}
|
||||
|
||||
// <form
|
||||
// ref={formRef}
|
||||
// className="flex flex-row items-center rounded-md p-2"
|
||||
// onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||
|
||||
// onSubmit={(e) => {
|
||||
// console.log('submit!')
|
||||
// onSubmit(e)
|
||||
// }}>
|
||||
// {/* input */}
|
||||
|
||||
// <textarea
|
||||
// ref={chatInputRef}
|
||||
// onChange={(e) => { setInstructions(e.target.value) }}
|
||||
// className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
||||
// placeholder="Ctrl+L to select"
|
||||
// rows={1}
|
||||
// onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
||||
// />
|
||||
// {isLoading ?
|
||||
// // stop button
|
||||
// <button
|
||||
// onClick={onAbort}
|
||||
// type='button'
|
||||
// className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
// >
|
||||
// <svg
|
||||
// className='scale-50'
|
||||
// stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
// <path d="M24 24H0V0h24v24z"></path>
|
||||
// </svg>
|
||||
// </button>
|
||||
// :
|
||||
// // submit button (up arrow)
|
||||
// <button
|
||||
// className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
// disabled={isDisabled}
|
||||
// type='submit'
|
||||
// >
|
||||
// <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
// <line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
// <polyline points="5 12 12 5 19 12"></polyline>
|
||||
// </svg>
|
||||
// </button>
|
||||
// }
|
||||
// </form>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {/* error message */}
|
||||
// {!latestError ? null : <div>
|
||||
// {latestError}
|
||||
// </div>}
|
||||
// </div>
|
||||
// </>
|
||||
// }
|
||||
import React, { FormEvent, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { generateDiffInstructions } from '../prompt/systemPrompts.js';
|
||||
|
||||
import { useConfigState, useService, useSidebarState, useThreadsState } from '../util/contextForServices.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { IFileService } from '../../../../../../../platform/files/common/files.js';
|
||||
import { userInstructionsStr } from '../prompt/stringifyFiles.js';
|
||||
import { sendLLMMessage } from '../util/sendLLMMessage.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { MarkdownRender } from '../markdown/MarkdownRender.js';
|
||||
|
||||
// read files from VSCode
|
||||
let VSReadFile = async (fileService: IFileService, filepath: URI): Promise<ChatFile | null> => {
|
||||
try {
|
||||
const fileObj = await fileService.readFile(filepath)
|
||||
const content = fileObj.value.toString()
|
||||
return { filepath, content }
|
||||
} catch (error) {
|
||||
console.error(`Failed to read ${filepath}:`, error);
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type ChatCodeSelection = { selectionStr: string; filePath: URI }
|
||||
|
||||
export type ChatFile = { filepath: URI; content: string }
|
||||
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string; // content sent to the llm
|
||||
displayContent: string; // content displayed to user
|
||||
selection: ChatCodeSelection | null; // the user's selection
|
||||
files: URI[]; // the files sent in the message
|
||||
}
|
||||
| {
|
||||
role: 'assistant';
|
||||
content: string; // content received from LLM
|
||||
displayContent: string | undefined; // content displayed to user (this is the same as content for now)
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getBasename = (pathStr: string) => {
|
||||
// 'unixify' path
|
||||
pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with /
|
||||
const parts = pathStr.split('/') // split on /
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
export const SelectedFiles = ({ files, setFiles, }: { files: URI[]; setFiles: null | ((files: URI[]) => void) }) => {
|
||||
return (
|
||||
files.length !== 0 && (
|
||||
<div className='flex flex-wrap -mx-1 -mb-1'>
|
||||
{files.map((filename, i) => (
|
||||
<button
|
||||
key={filename.path}
|
||||
disabled={!setFiles}
|
||||
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
|
||||
type='button'
|
||||
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
|
||||
>
|
||||
<span>{getBasename(filename.fsPath)}</span>
|
||||
|
||||
{/* X button */}
|
||||
{!!setFiles && <span className=''>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
className='size-4'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
d='M6 18 18 6M6 6l12 12'
|
||||
/>
|
||||
</svg>
|
||||
</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
const children = chatMessage.displayContent
|
||||
|
||||
if (!children)
|
||||
return null
|
||||
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles files={chatMessage.files} setFiles={null} />
|
||||
{chatMessage.selection?.selectionStr && <BlockCode
|
||||
text={chatMessage.selection.selectionStr}
|
||||
buttonsOnHover={null}
|
||||
/>}
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
|
||||
}
|
||||
|
||||
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
||||
{chatbubbleContents}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HTMLTextAreaElement> }) => {
|
||||
|
||||
const fileService = useService('fileService')
|
||||
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const [sidebarState, sidebarStateService] = useSidebarState()
|
||||
const { isHistoryOpen, currentTab: tab } = sidebarState
|
||||
sidebarStateService.onDidFocusChat(() => { chatInputRef.current?.focus() })
|
||||
sidebarStateService.onDidBlurChat(() => { chatInputRef.current?.blur() })
|
||||
|
||||
// config state
|
||||
const [configState, configStateService] = useConfigState()
|
||||
const { voidConfig } = configState
|
||||
|
||||
// threads state
|
||||
const [threadsState, threadsStateService] = useThreadsState()
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
// state of current message
|
||||
const [selection, setSelection] = useState<ChatCodeSelection | null>(null) // the code the user is selecting
|
||||
const [files, setFiles] = useState<URI[]>([]) // the names of the files in the chat
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
|
||||
// state of chat
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const abortFnRef = useRef<(() => void) | null>(null)
|
||||
|
||||
const [latestError, setLatestError] = useState('')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const isDisabled = !instructions
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
if (isDisabled) return
|
||||
if (isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
setInstructions('');
|
||||
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
|
||||
setSelection(null)
|
||||
setFiles([])
|
||||
setLatestError('')
|
||||
|
||||
|
||||
|
||||
const relevantFiles = await Promise.all(
|
||||
files.map((filepath) => VSReadFile(fileService, filepath))
|
||||
).then(
|
||||
(files) => files.filter(file => file !== null)
|
||||
)
|
||||
|
||||
// add system message to chat history
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
|
||||
|
||||
threadsStateService.addMessageToCurrentThread(systemPromptElt)
|
||||
|
||||
const userContent = userInstructionsStr(instructions, relevantFiles, selection)
|
||||
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread()
|
||||
|
||||
|
||||
// send message to LLM
|
||||
sendLLMMessage({
|
||||
logging: { loggingName: 'Chat' },
|
||||
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
|
||||
onText: (newText, fullText) => setMessageStream(fullText),
|
||||
onFinalMessage: (content) => {
|
||||
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
let content = messageStream; // just use the current content
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
setLatestError(error)
|
||||
},
|
||||
voidConfig,
|
||||
abortRef: abortFnRef,
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
const onAbort = useCallback(() => {
|
||||
// abort claude
|
||||
abortFnRef.current?.()
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream || '(null)'
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
|
||||
threadsStateService.addMessageToCurrentThread(newHistoryElt)
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
}, [messageStream, threadsStateService])
|
||||
|
||||
|
||||
const currentThread = threadsStateService.getCurrentThread()
|
||||
return <>
|
||||
<div className="overflow-x-hidden space-y-4">
|
||||
{/* previous messages */}
|
||||
{currentThread !== null && currentThread?.messages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
|
||||
</div>
|
||||
{/* chatbar */}
|
||||
<div className="shrink-0 py-4">
|
||||
{/* selection */}
|
||||
<div className="text-left">
|
||||
<div className="relative">
|
||||
<div className="input">
|
||||
{/* selection */}
|
||||
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
||||
{/* selected files */}
|
||||
<SelectedFiles files={files} setFiles={setFiles} />
|
||||
{/* selected code */}
|
||||
{!!selection?.selectionStr && (
|
||||
<BlockCode text={selection.selectionStr}
|
||||
buttonsOnHover={(
|
||||
<button
|
||||
onClick={() => setSelection(null)}
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)} />
|
||||
)}
|
||||
</div>}
|
||||
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}>
|
||||
{/* input */}
|
||||
|
||||
<textarea
|
||||
ref={chatInputRef}
|
||||
onChange={(e) => { setInstructions(e.target.value) }}
|
||||
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
||||
placeholder="Ctrl+L to select"
|
||||
rows={1}
|
||||
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
||||
/>
|
||||
{isLoading ?
|
||||
// stop button
|
||||
<button
|
||||
onClick={onAbort}
|
||||
type='button'
|
||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
>
|
||||
<svg
|
||||
className='scale-50'
|
||||
stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 24H0V0h24v24z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<button
|
||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
disabled={isDisabled}
|
||||
type='submit'
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* error message */}
|
||||
{!latestError ? null : <div>
|
||||
{latestError}
|
||||
</div>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useServices } from '../util/contextForServices.js';
|
||||
import { useConfigState } from '../util/contextForServices.js';
|
||||
import { IVoidConfigStateService, nonDefaultConfigFields, PartialVoidConfig, VoidConfig, VoidConfigField, VoidConfigInfo, SetFieldFnType, ConfigState } from '../../../registerConfig.js';
|
||||
|
||||
|
||||
const SettingOfFieldAndParam = ({ field, param, configState, configStateService }:
|
||||
{ field: VoidConfigField; param: string; configState: NonNullable<ConfigState>; configStateService: IVoidConfigStateService }) => {
|
||||
{ field: VoidConfigField; param: string; configState: ConfigState; configStateService: IVoidConfigStateService }) => {
|
||||
|
||||
const { partialVoidConfig } = configState
|
||||
|
||||
|
|
@ -60,12 +60,9 @@ const SettingOfFieldAndParam = ({ field, param, configState, configStateService
|
|||
|
||||
|
||||
export const SidebarSettings = () => {
|
||||
// track the config state using React state so visual updates happen
|
||||
const { configStateService } = useServices()
|
||||
const [configState, setConfigState] = useState<ConfigState>(configStateService.state)
|
||||
const { voidConfig } = configState
|
||||
useEffect(() => { configStateService.onDidChangeState(() => setConfigState(configStateService.state)) }, [configStateService])
|
||||
|
||||
const [configState, configStateService] = useConfigState()
|
||||
const { voidConfig } = configState
|
||||
const current_field = voidConfig.default['whichApi'] as VoidConfigField
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,79 +1,79 @@
|
|||
// import React from "react";
|
||||
// import { ThreadsProvider, useThreads } from "../util/contextForThreads";
|
||||
import React from "react";
|
||||
import { useThreadsState } from '../util/contextForServices';
|
||||
|
||||
|
||||
// const truncate = (s: string) => {
|
||||
// let len = s.length
|
||||
// const TRUNC_AFTER = 16
|
||||
// if (len >= TRUNC_AFTER)
|
||||
// s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
// return s
|
||||
// }
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
// export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
// const { getAllThreads, getCurrentThread, switchToThread } = useThreads()
|
||||
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
const [threadsState, threadsStateService] = useThreadsState()
|
||||
|
||||
// const allThreads = getAllThreads()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// // sorted by most recent to least recent
|
||||
// const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
|
||||
|
||||
// return (
|
||||
// <div className="flex flex-col gap-y-1">
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
|
||||
// {/* X button at top right */}
|
||||
// <div className="text-right">
|
||||
// <button className="btn btn-sm" onClick={onClose}>
|
||||
// <svg
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// fill="none"
|
||||
// viewBox="0 0 24 24"
|
||||
// stroke="currentColor"
|
||||
// className="size-4"
|
||||
// >
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// d="M6 18 18 6M6 6l12 12"
|
||||
// />
|
||||
// </svg>
|
||||
// </button>
|
||||
// </div>
|
||||
{/* X button at top right */}
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
// {/* a list of all the past threads */}
|
||||
// <div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
|
||||
// {sortedThreadIds.map((threadId) => {
|
||||
// if (!allThreads)
|
||||
// return <>Error: Threads not found.</>
|
||||
// const pastThread = allThreads[threadId]
|
||||
{/* a list of all the past threads */}
|
||||
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
|
||||
{sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads)
|
||||
return <>Error: Threads not found.</>
|
||||
const pastThread = allThreads[threadId]
|
||||
|
||||
// let btnStringArr = []
|
||||
let btnStringArr: string[] = []
|
||||
|
||||
// let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
|
||||
// btnStringArr.push(msg1)
|
||||
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
|
||||
btnStringArr.push(msg1)
|
||||
|
||||
// let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
|
||||
// if (msg2)
|
||||
// btnStringArr.push(msg2)
|
||||
let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
|
||||
if (msg2)
|
||||
btnStringArr.push(msg2)
|
||||
|
||||
// btnStringArr.push(allThreads[threadId].messages.length)
|
||||
btnStringArr.push(allThreads[threadId].messages.length + '')
|
||||
|
||||
// const btnString = btnStringArr.join(' / ')
|
||||
const btnString = btnStringArr.join(' / ')
|
||||
|
||||
// return (
|
||||
// <button
|
||||
// key={pastThread.id}
|
||||
// className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
|
||||
// onClick={() => switchToThread(pastThread.id)}
|
||||
// title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
// >
|
||||
// {btnString}
|
||||
// </button>
|
||||
// )
|
||||
// })}
|
||||
// </div>
|
||||
return (
|
||||
<button
|
||||
key={pastThread.id}
|
||||
className={`btn btn-sm rounded-sm ${pastThread.id === threadsStateService.getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
|
||||
onClick={() => threadsStateService.switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
{btnString}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
|
||||
// import * as vscode from 'vscode';
|
||||
// import { PartialVoidConfig } from '../webviews/common/contextForConfig'
|
||||
|
||||
// // type CodeSelection = { selectionStr: string, filePath: vscode.Uri }
|
||||
|
||||
// // type File = { filepath: vscode.Uri, content: string }
|
||||
|
||||
// // an area that is currently being diffed
|
||||
// type DiffArea = {
|
||||
// diffareaid: number,
|
||||
// startLine: number,
|
||||
// endLine: number,
|
||||
// originalStartLine: number,
|
||||
// originalEndLine: number,
|
||||
// sweepIndex: number | null // null iff not sweeping
|
||||
// }
|
||||
|
||||
// // the return type of diff creator
|
||||
// type BaseDiff = {
|
||||
// type: 'edit' | 'insertion' | 'deletion';
|
||||
// // repr: string; // representation of the diff in text
|
||||
// originalRange: vscode.Range;
|
||||
// originalCode: string;
|
||||
// range: vscode.Range;
|
||||
// code: string;
|
||||
// }
|
||||
|
||||
// // each diff on the user's screen
|
||||
// type Diff = {
|
||||
// diffid: number,
|
||||
// lenses: vscode.CodeLens[],
|
||||
// } & BaseDiff
|
||||
|
||||
// // editor -> sidebar
|
||||
// type MessageToSidebar = (
|
||||
// | { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor. selection and path are frozen snapshots
|
||||
// | { type: 'ctrl+k', selection: CodeSelection }
|
||||
// | { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
||||
// | { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
|
||||
// | { type: 'allThreads', threads: ChatThreads }
|
||||
// | { type: 'startNewThread' }
|
||||
// | { type: 'toggleThreadSelector' }
|
||||
// | { type: 'toggleSettings' }
|
||||
// | { type: 'deviceId', deviceId: string }
|
||||
// )
|
||||
|
||||
// // sidebar -> editor
|
||||
// type MessageFromSidebar = (
|
||||
// | { type: 'applyChanges', diffRepr: string } // user clicks "apply" in the sidebar
|
||||
// | { type: 'requestFiles', filepaths: vscode.Uri[] }
|
||||
// | { type: 'getPartialVoidConfig' }
|
||||
// | { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
|
||||
// | { type: 'getAllThreads' }
|
||||
// | { type: 'persistThread', thread: ChatThreads[string] }
|
||||
// | { type: 'getDeviceId' }
|
||||
// )
|
||||
|
||||
|
||||
// // type ChatThreads = {
|
||||
// // [id: string]: {
|
||||
// // id: string; // store the id here too
|
||||
// // createdAt: string; // ISO string
|
||||
// // lastModified: string; // ISO string
|
||||
// // messages: ChatMessage[];
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // type ChatMessage =
|
||||
// // | {
|
||||
// // role: "user";
|
||||
// // content: string; // content sent to the llm
|
||||
// // displayContent: string; // content displayed to user
|
||||
// // selection: CodeSelection | null; // the user's selection
|
||||
// // files: vscode.Uri[]; // the files sent in the message
|
||||
// // }
|
||||
// // | {
|
||||
// // role: "assistant";
|
||||
// // content: string; // content received from LLM
|
||||
// // displayContent: string | undefined; // content displayed to user (this is the same as content for now)
|
||||
// // }
|
||||
// // | {
|
||||
// // role: "system";
|
||||
// // content: string;
|
||||
// // displayContent?: undefined;
|
||||
// // }
|
||||
|
||||
// export {
|
||||
// BaseDiff, Diff,
|
||||
// DiffArea,
|
||||
// CodeSelection,
|
||||
// File,
|
||||
// MessageFromSidebar,
|
||||
// MessageToSidebar,
|
||||
// ChatThreads,
|
||||
// ChatMessage,
|
||||
// }
|
||||
|
|
@ -2,3 +2,43 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
/* html {
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply cursor-pointer transition-colors;
|
||||
|
||||
&.btn-primary {
|
||||
@apply bg-vscode-button-bg text-vscode-button-fg;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-sm {
|
||||
@apply px-3 py-1 text-sm;
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-secondary-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-75 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border focus:outline-vscode-focus-border;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border;
|
||||
} */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { createContext, useContext } from 'react'
|
||||
import { ReactServicesType } from '../../../registerSidebar.js';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { ReactServicesType, VoidSidebarState } from '../../../registerSidebar.js';
|
||||
import { ConfigState } from '../../../registerConfig.js';
|
||||
|
||||
const AccessorContext = createContext<ReactServicesType | undefined>(undefined)
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ export const AccessorProvider = ({ children, services }: { children: React.React
|
|||
</AccessorContext.Provider>
|
||||
}
|
||||
|
||||
export const useServices = (): ReactServicesType => {
|
||||
const useServices = (): ReactServicesType => {
|
||||
const context = useContext(AccessorContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useAccessor must be used within an AccessorProvider')
|
||||
|
|
@ -17,4 +18,33 @@ export const useServices = (): ReactServicesType => {
|
|||
return context;
|
||||
}
|
||||
|
||||
// -- these use useServices() --
|
||||
// track the config state using React state so visual updates happen
|
||||
export const useSidebarState = () => {
|
||||
const { sidebarStateService } = useServices()
|
||||
const [sidebarState, setSideBarState] = useState<VoidSidebarState>(sidebarStateService.state)
|
||||
useEffect(() => { sidebarStateService.onDidChangeState(() => setSideBarState(sidebarStateService.state)) }, [sidebarStateService])
|
||||
return [sidebarState, sidebarStateService] as const
|
||||
}
|
||||
|
||||
export const useConfigState = () => {
|
||||
const { configStateService } = useServices()
|
||||
const [configState, setConfigState] = useState<ConfigState>(configStateService.state)
|
||||
useEffect(() => { configStateService.onDidChangeState(() => setConfigState(configStateService.state)) }, [configStateService])
|
||||
return [configState, configStateService] as const
|
||||
}
|
||||
|
||||
export const useThreadsState = () => {
|
||||
const { threadsStateService } = useServices()
|
||||
const [threadsState, setThreadsState] = useState(threadsStateService.state)
|
||||
useEffect(() => { threadsStateService.onDidChangeCurrentThread(() => setThreadsState(threadsStateService.state)) }, [threadsStateService])
|
||||
return [threadsState, threadsStateService] as const
|
||||
}
|
||||
|
||||
// -- other services --
|
||||
type PublicServiceName = 'fileService'
|
||||
export const useService = (serviceName: Extract<keyof ReactServicesType, PublicServiceName>) => {
|
||||
const services = useServices()
|
||||
return services[serviceName]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,395 @@
|
|||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { Ollama } from 'ollama/browser'
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
// import { VoidConfig } from '../webviews/common/contextForConfig'
|
||||
// import { captureEvent } from '../webviews/common/posthog';
|
||||
// import { ChatMessage } from './shared_types';
|
||||
|
||||
type VoidConfig = any
|
||||
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type OnText = (newText: string, fullText: string) => void
|
||||
|
||||
export type OnFinalMessage = (input: string) => void
|
||||
|
||||
export type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
type SendLLMMessageFnTypeInternal = (params: {
|
||||
messages: LLMMessage[];
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: (error: string) => void;
|
||||
voidConfig: VoidConfig;
|
||||
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}) => void
|
||||
|
||||
type SendLLMMessageFnTypeExternal = (params: {
|
||||
messages: LLMMessage[];
|
||||
onText: OnText;
|
||||
onFinalMessage: (fullText: string) => void;
|
||||
onError: (error: string) => void;
|
||||
voidConfig: VoidConfig | null;
|
||||
abortRef: AbortRef;
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
}) => void
|
||||
|
||||
const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
if (Number.isNaN(int))
|
||||
return undefined
|
||||
return int
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// remove system messages for Anthropic
|
||||
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
model: voidConfig.anthropic.model,
|
||||
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
|
||||
});
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText(newText, fullText)
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
// stringify the response's content
|
||||
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
onFinalMessage(content)
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
// the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
onError('Invalid API key.')
|
||||
}
|
||||
else {
|
||||
onError(error.message)
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
|
||||
// Gemini
|
||||
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
|
||||
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
|
||||
|
||||
// remove system messages that get sent to Gemini
|
||||
// str of all system messages
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const geminiMessages: Content[] = messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg, i) => ({
|
||||
parts: [{ text: msg.content }],
|
||||
role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
}))
|
||||
|
||||
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
const newText = chunk.text();
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof GoogleGenerativeAIFetchError) {
|
||||
if (error.status === 400) {
|
||||
onError('Invalid API key.');
|
||||
}
|
||||
else {
|
||||
onError(`${error.name}:\n${error.message}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
onError(error);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
|
||||
|
||||
if (voidConfig.default.whichApi === 'openAI') {
|
||||
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
|
||||
options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
}
|
||||
else if (voidConfig.default.whichApi === 'openRouter') {
|
||||
openai = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
});
|
||||
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
}
|
||||
else if (voidConfig.default.whichApi === 'openAICompatible') {
|
||||
openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
|
||||
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
|
||||
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError) {
|
||||
if (error.status === 401) {
|
||||
onError('Invalid API key.');
|
||||
}
|
||||
else {
|
||||
onError(`${error.name}:\n${error.message}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
onError(error);
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: voidConfig.ollama.model,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch(error => {
|
||||
onError(error)
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
// Greptile
|
||||
// https://docs.greptile.com/api-reference/query
|
||||
// https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
fetch('https://api.greptile.com/v2/query', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
|
||||
'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
|
||||
'Content-Type': `application/json`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
stream: true,
|
||||
repositories: [voidConfig.greptile.repoinfo],
|
||||
}),
|
||||
})
|
||||
// this is {message}\n{message}\n{message}...\n
|
||||
.then(async response => {
|
||||
const text = await response.text()
|
||||
console.log('got greptile', text)
|
||||
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
})
|
||||
// TODO make this actually stream, right now it just sends one message at the end
|
||||
// TODO add _setAborter() when add streaming
|
||||
.then(async responseArr => {
|
||||
|
||||
for (const response of responseArr) {
|
||||
const type: string = response['type']
|
||||
const message = response['message']
|
||||
|
||||
// when receive text
|
||||
if (type === 'message') {
|
||||
fullText += message
|
||||
onText(message, fullText)
|
||||
}
|
||||
else if (type === 'sources') {
|
||||
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
|
||||
fullText += filepath
|
||||
onText(filepath, fullText)
|
||||
}
|
||||
// type: 'status' with an empty 'message' means last message
|
||||
else if (type === 'status') {
|
||||
if (!message) {
|
||||
onFinalMessage(fullText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(e => {
|
||||
onError(e)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
|
||||
messages,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
voidConfig,
|
||||
logging: { loggingName }
|
||||
}) => {
|
||||
if (!voidConfig) return;
|
||||
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
// captureEvent(eventId, {
|
||||
// whichApi: voidConfig.default['whichApi'],
|
||||
// numMessages: messages?.length,
|
||||
// messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
// version: '2024-11-02',
|
||||
// ...extras,
|
||||
// })
|
||||
}
|
||||
const submit_time = new Date()
|
||||
|
||||
let _fullTextSoFar = ''
|
||||
let _aborter: (() => void) | null = null
|
||||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText = (newText: string, fullText: string) => {
|
||||
if (_didAbort) return
|
||||
onText_(newText, fullText)
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage = (fullText: string) => {
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_(fullText)
|
||||
}
|
||||
|
||||
const onError = (error: string) => {
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_(error)
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
_aborter?.()
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
switch (voidConfig.default.whichApi) {
|
||||
case 'anthropic':
|
||||
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'ollama':
|
||||
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
case 'greptile':
|
||||
sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
|
||||
break;
|
||||
default:
|
||||
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -2,7 +2,29 @@
|
|||
module.exports = {
|
||||
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
colors: {
|
||||
vscode: {
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"button-fg": "var(--vscode-button-foreground)",
|
||||
"button-bg": "var(--vscode-button-background)",
|
||||
"button-hoverBg": "var(--vscode-button-hoverBackground)",
|
||||
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
|
||||
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
|
||||
"button-secondary-hoverBg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
"dropdown-bg": "var(--vscode-settings-dropdownBackground)",
|
||||
"dropdown-foreground": "var(--vscode-settings-dropdownForeground)",
|
||||
"dropdown-border": "var(--vscode-settings-dropdownBorder)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
prefix: 'prefix-'
|
||||
|
|
|
|||
11
src/vs/workbench/contrib/void/browser/react/tsconfig.json
Normal file
11
src/vs/workbench/contrib/void/browser/react/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
]
|
||||
}
|
||||
|
|
@ -220,8 +220,8 @@ const VOID_CONFIG_KEY = 'void.partialVoidConfig'
|
|||
export type SetFieldFnType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => Promise<void>;
|
||||
|
||||
export type ConfigState = {
|
||||
partialVoidConfig: PartialVoidConfig;
|
||||
voidConfig: VoidConfig;
|
||||
partialVoidConfig: PartialVoidConfig; // free parameter
|
||||
voidConfig: VoidConfig; // computed from partialVoidConfig
|
||||
}
|
||||
|
||||
export interface IVoidConfigStateService {
|
||||
|
|
@ -240,51 +240,7 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi
|
|||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
state: ConfigState;
|
||||
|
||||
voidConfigInfo: VoidConfigInfo = voidConfigInfo;
|
||||
|
||||
|
||||
private async _readPartialVoidConfig(): Promise<PartialVoidConfig> {
|
||||
const encryptedPartialConfig = this._storageService.get(VOID_CONFIG_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedPartialConfig)
|
||||
return {}
|
||||
|
||||
const partialVoidConfigStr = await this._encryptionService.decrypt(encryptedPartialConfig)
|
||||
return JSON.parse(partialVoidConfigStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) {
|
||||
const encryptedPartialConfigStr = await this._encryptionService.encrypt(JSON.stringify(partialVoidConfig))
|
||||
this._storageService.store(VOID_CONFIG_KEY, encryptedPartialConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
|
||||
// Set field on PartialVoidConfig
|
||||
setField: SetFieldFnType = async <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => {
|
||||
const partialVoidConfig = await this._readPartialVoidConfig()
|
||||
|
||||
const newPartialConfig: PartialVoidConfig = {
|
||||
...partialVoidConfig,
|
||||
[field]: {
|
||||
...partialVoidConfig[field],
|
||||
[param]: newVal
|
||||
}
|
||||
}
|
||||
await this._storePartialVoidConfig(newPartialConfig)
|
||||
this._setState(newPartialConfig)
|
||||
}
|
||||
|
||||
// internal function to update state, should be called every time state changes
|
||||
private async _setState(partialVoidConfig: PartialVoidConfig) {
|
||||
this.state = {
|
||||
partialVoidConfig: partialVoidConfig,
|
||||
voidConfig: getVoidConfig(partialVoidConfig),
|
||||
}
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
readonly voidConfigInfo: VoidConfigInfo = voidConfigInfo; // just putting this here for simplicity, it's static though
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
|
|
@ -307,6 +263,47 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi
|
|||
|
||||
}
|
||||
|
||||
private async _readPartialVoidConfig(): Promise<PartialVoidConfig> {
|
||||
const encryptedPartialConfig = this._storageService.get(VOID_CONFIG_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedPartialConfig)
|
||||
return {}
|
||||
|
||||
const partialVoidConfigStr = await this._encryptionService.decrypt(encryptedPartialConfig)
|
||||
return JSON.parse(partialVoidConfigStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) {
|
||||
const encryptedPartialConfigStr = await this._encryptionService.encrypt(JSON.stringify(partialVoidConfig))
|
||||
this._storageService.store(VOID_CONFIG_KEY, encryptedPartialConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
|
||||
// Set field on PartialVoidConfig
|
||||
setField: SetFieldFnType = async <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => {
|
||||
const { partialVoidConfig } = this.state
|
||||
|
||||
const newPartialConfig: PartialVoidConfig = {
|
||||
...partialVoidConfig,
|
||||
[field]: {
|
||||
...partialVoidConfig[field],
|
||||
[param]: newVal
|
||||
}
|
||||
}
|
||||
await this._storePartialVoidConfig(newPartialConfig)
|
||||
this._setState(newPartialConfig)
|
||||
}
|
||||
|
||||
// internal function to update state, should be called every time state changes
|
||||
private async _setState(partialVoidConfig: PartialVoidConfig) {
|
||||
this.state = {
|
||||
partialVoidConfig: partialVoidConfig,
|
||||
voidConfig: getVoidConfig(partialVoidConfig),
|
||||
}
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidConfigStateService, VoidConfigStateService, InstantiationType.Eager);
|
||||
|
|
|
|||
339
src/vs/workbench/contrib/void/browser/registerInlineDiff.ts
Normal file
339
src/vs/workbench/contrib/void/browser/registerInlineDiff.ts
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
|
||||
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 { IModelDeltaDecoration } from '../../../common/model.js';
|
||||
import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
|
||||
import { IRange } from '../../../common/core/range.js';
|
||||
import { EditorOption } from '../../../common/config/editorOptions.js';
|
||||
|
||||
|
||||
// else if (m.type === 'applyChanges') {
|
||||
|
||||
// const editor = vscode.window.activeTextEditor
|
||||
// if (!editor) {
|
||||
// vscode.window.showInformationMessage('No active editor!')
|
||||
// return
|
||||
// }
|
||||
// // create an area to show diffs
|
||||
// const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
|
||||
// startLine: 0, // in ctrl+L the start and end lines are the full document
|
||||
// endLine: editor.document.lineCount,
|
||||
// originalStartLine: 0,
|
||||
// originalEndLine: editor.document.lineCount,
|
||||
// sweepIndex: null,
|
||||
// }
|
||||
// const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
|
||||
|
||||
// const docUri = editor.document.uri
|
||||
// const fileStr = await readFileContentOfUri(docUri)
|
||||
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
|
||||
|
||||
// await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffArea, abortRef: abortApplyRef })
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// // an area that is currently being diffed
|
||||
// type DiffArea = {
|
||||
// diffareaid: number,
|
||||
// startLine: number,
|
||||
// endLine: number,
|
||||
// originalStartLine: number,
|
||||
// originalEndLine: number,
|
||||
// sweepIndex: number | null // null iff not sweeping
|
||||
// }
|
||||
|
||||
// // the return type of diff creator
|
||||
// type BaseDiff = {
|
||||
// type: 'edit' | 'insertion' | 'deletion';
|
||||
// // repr: string; // representation of the diff in text
|
||||
// originalRange: vscode.Range;
|
||||
// originalCode: string;
|
||||
// range: vscode.Range;
|
||||
// code: string;
|
||||
// }
|
||||
|
||||
// // each diff on the user's screen
|
||||
// type Diff = {
|
||||
// diffid: number,
|
||||
// lenses: vscode.CodeLens[],
|
||||
// } & BaseDiff
|
||||
|
||||
|
||||
|
||||
export interface IInlineDiffService {
|
||||
readonly _serviceBrand: undefined;
|
||||
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
|
||||
removeDiffs(editor: ICodeEditor): void;
|
||||
}
|
||||
|
||||
export const IInlineDiffService = createDecorator<IInlineDiffService>('inlineDiffService');
|
||||
|
||||
class InlineDiffService extends Disposable implements IInlineDiffService {
|
||||
private readonly _diffDecorations = new Map<ICodeEditor, string[]>();
|
||||
private readonly _diffZones = new Map<ICodeEditor, string[]>();
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
initStream() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public addDiff: IInlineDiffService['addDiff'] = (editor, originalText, modifiedRange) => {
|
||||
// Clear existing diffs
|
||||
this.removeDiffs(editor);
|
||||
|
||||
// green decoration and gutter decoration
|
||||
const greenDecoration: IModelDeltaDecoration[] = [{
|
||||
range: modifiedRange,
|
||||
options: {
|
||||
className: 'line-insert', // .monaco-editor .line-insert
|
||||
description: 'line-insert',
|
||||
isWholeLine: true,
|
||||
minimap: {
|
||||
color: { id: 'minimapGutter.addedBackground' },
|
||||
position: 2
|
||||
},
|
||||
overviewRuler: {
|
||||
color: { id: 'editorOverviewRuler.addedForeground' },
|
||||
position: 7
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
this._diffDecorations.set(editor, editor.deltaDecorations([], greenDecoration));
|
||||
|
||||
// red in a view zone
|
||||
editor.changeViewZones(accessor => {
|
||||
// Get the editor's font info
|
||||
const fontInfo = editor.getOption(EditorOption.fontInfo);
|
||||
|
||||
const domNode = document.createElement('div');
|
||||
domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text';
|
||||
domNode.style.fontSize = `${fontInfo.fontSize}px`;
|
||||
domNode.style.fontFamily = fontInfo.fontFamily;
|
||||
domNode.style.lineHeight = `${fontInfo.lineHeight}px`;
|
||||
|
||||
// div
|
||||
const lineContent = document.createElement('div');
|
||||
lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text
|
||||
|
||||
// span
|
||||
const contentSpan = document.createElement('span');
|
||||
|
||||
// span
|
||||
const codeSpan = document.createElement('span');
|
||||
codeSpan.className = 'mtk1'; // char-delete
|
||||
codeSpan.textContent = originalText;
|
||||
|
||||
// Mount
|
||||
contentSpan.appendChild(codeSpan);
|
||||
lineContent.appendChild(contentSpan);
|
||||
domNode.appendChild(lineContent);
|
||||
|
||||
const viewZone: IViewZone = {
|
||||
afterLineNumber: modifiedRange.startLineNumber - 1,
|
||||
heightInLines: originalText.split('\n').length + 1,
|
||||
domNode: domNode,
|
||||
suppressMouseDown: true,
|
||||
marginDomNode: this.createGutterElement()
|
||||
};
|
||||
|
||||
const zoneId = accessor.addZone(viewZone);
|
||||
// editor.layout();
|
||||
this._diffZones.set(editor, [zoneId]);
|
||||
});
|
||||
}
|
||||
|
||||
// gutter is the thing to the left
|
||||
private createGutterElement(): HTMLElement {
|
||||
const gutterDiv = document.createElement('div');
|
||||
gutterDiv.className = 'inline-diff-gutter';
|
||||
|
||||
const minusDiv = document.createElement('div');
|
||||
minusDiv.className = 'inline-diff-deleted-gutter';
|
||||
// minusDiv.textContent = '-';
|
||||
|
||||
gutterDiv.appendChild(minusDiv);
|
||||
return gutterDiv;
|
||||
}
|
||||
|
||||
public removeDiffs(editor: ICodeEditor): void {
|
||||
const decorationIds = this._diffDecorations.get(editor) || [];
|
||||
editor.deltaDecorations(decorationIds, []);
|
||||
this._diffDecorations.delete(editor);
|
||||
|
||||
editor.changeViewZones(accessor => {
|
||||
const zoneIds = this._diffZones.get(editor) || [];
|
||||
zoneIds.forEach(id => accessor.removeZone(id));
|
||||
});
|
||||
this._diffZones.delete(editor);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this._diffDecorations.clear();
|
||||
this._diffZones.clear();
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IInlineDiffService, InlineDiffService, InstantiationType.Eager);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // Void created this file
|
||||
// // it comes from mainThreadCodeInsets.ts
|
||||
|
||||
// import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
// import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
|
||||
// import { MainContext, MainThreadInlineDiffShape } from '../common/extHost.protocol.js';
|
||||
// import { IInlineDiffService } from '../../../editor/browser/services/inlineDiffService/inlineDiffService.js';
|
||||
// import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';
|
||||
// import { IRange } from '../../../editor/common/core/range.js';
|
||||
// import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
|
||||
// import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
|
||||
// import { IBulkEditService } from '../../../editor/browser/services/bulkEditService.js';
|
||||
// import { WorkspaceEdit } from '../../../editor/common/languages.js';
|
||||
// // import { IHistoryService } from '../../services/history/common/history.js';
|
||||
|
||||
|
||||
// @extHostNamedCustomer(MainContext.MainThreadInlineDiff)
|
||||
// export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
|
||||
|
||||
// // private readonly _proxy: ExtHostEditorInsetsShape;
|
||||
// // private readonly _disposables = new DisposableStore();
|
||||
|
||||
// constructor(
|
||||
// context: IExtHostContext,
|
||||
// @IInlineDiffService private readonly _inlineDiff: IInlineDiffService,
|
||||
// @ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
// // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
|
||||
// @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
|
||||
// @IBulkEditService private readonly _bulkEditService: IBulkEditService,
|
||||
|
||||
// ) {
|
||||
// super();
|
||||
|
||||
// // this._proxy = context.getProxy(ExtHostContext.ExtHostEditorInsets);
|
||||
// // this._wcHistoryService.addEntry()
|
||||
// }
|
||||
|
||||
// _streamingState: { type: 'streaming'; editGroup: UndoRedoGroup } | { type: 'idle' } = { type: 'idle' }
|
||||
|
||||
// startStreaming(editorId: string) {
|
||||
// const editor = this._getEditor(editorId)
|
||||
// if (!editor) return
|
||||
|
||||
// const model = editor.getModel()
|
||||
// if (!model) return
|
||||
|
||||
// // all changes made when streaming should be a part of the group so we can undo them all together
|
||||
// this._streamingState = {
|
||||
// type: 'streaming',
|
||||
// editGroup: new UndoRedoGroup()
|
||||
// }
|
||||
|
||||
// // TODO probably need to convert this to a stack
|
||||
// const diffsSnapshotBefore = { placeholder: '' }
|
||||
// const diffsSnapshotAfter = { placeholder: '' }
|
||||
|
||||
// const elt: IUndoRedoElement = {
|
||||
// type: UndoRedoElementType.Resource,
|
||||
// resource: model.uri,
|
||||
// label: 'Add Diffs',
|
||||
// code: 'undoredo.inlineDiff',
|
||||
// undo: () => {
|
||||
// // reapply diffareas and diffs here
|
||||
// console.log('reverting diffareas...', diffsSnapshotBefore.placeholder)
|
||||
// },
|
||||
// redo: () => {
|
||||
// // reapply diffareas and diffs here
|
||||
// // when done, need to record diffSnapshotAfter
|
||||
// console.log('re-applying diffareas...', diffsSnapshotAfter.placeholder)
|
||||
// }
|
||||
// }
|
||||
|
||||
// this._undoRedoService.pushElement(elt, this._streamingState.editGroup)
|
||||
|
||||
// // ---------- START ----------
|
||||
// editor.updateOptions({ readOnly: true })
|
||||
|
||||
|
||||
|
||||
// // ---------- WHEN DONE ----------
|
||||
// editor.updateOptions({ readOnly: false })
|
||||
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// streamChange(editorId: string, edit: WorkspaceEdit) {
|
||||
// const editor = this._getEditor(editorId)
|
||||
// if (!editor) return
|
||||
|
||||
// if (this._streamingState.type !== 'streaming') {
|
||||
// console.error('Expected streamChange to be in state \'streaming\'.')
|
||||
// return
|
||||
// }
|
||||
|
||||
// // count all changes towards the group
|
||||
// this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, })
|
||||
|
||||
|
||||
// }
|
||||
|
||||
// _getEditor = (editorId: string): ICodeEditor | undefined => {
|
||||
|
||||
// let editor: ICodeEditor | undefined;
|
||||
// editorId = editorId.substr(0, editorId.indexOf(',')); //todo@jrieken HACK
|
||||
|
||||
// for (const candidate of this._editorService.listCodeEditors()) {
|
||||
// if (candidate.getId() === editorId
|
||||
// // && candidate.hasModel() && isEqual(candidate.getModel().uri, URI.revive(uri))
|
||||
// ) {
|
||||
// editor = candidate;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// return editor
|
||||
// }
|
||||
|
||||
|
||||
// $addDiff(editorId: string, originalText: string, range: IRange): void {
|
||||
|
||||
// const editor = this._getEditor(editorId);
|
||||
// if (!editor) return
|
||||
|
||||
// this._inlineDiff.addDiff(editor, originalText, range)
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// }
|
||||
|
|
@ -28,7 +28,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
|
|||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { IThreadHistoryService } from './registerThreadsHistory.js';
|
||||
import { IThreadHistoryService } from './registerThreads.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
|
|
@ -42,6 +42,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js';
|
|||
import mountFn from './react/out/sidebar-tsx/Sidebar.js';
|
||||
|
||||
import { IVoidConfigStateService } from './registerConfig.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
|
||||
|
||||
// const mountFn = (...params: any) => { }
|
||||
|
|
@ -58,7 +59,8 @@ export type VoidSidebarState = {
|
|||
export type ReactServicesType = {
|
||||
sidebarStateService: IVoidSidebarStateService;
|
||||
configStateService: IVoidConfigStateService;
|
||||
threadHistoryService: IThreadHistoryService;
|
||||
threadsStateService: IThreadHistoryService;
|
||||
fileService: IFileService;
|
||||
}
|
||||
|
||||
// ---------- Define viewpane ----------
|
||||
|
|
@ -198,7 +200,7 @@ viewsRegistry.registerViews([{
|
|||
export interface IVoidSidebarStateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
state: VoidSidebarState;
|
||||
readonly state: VoidSidebarState; // readonly to the user
|
||||
setState(newState: Partial<VoidSidebarState>): void;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,16 +8,7 @@ import { Emitter, Event } from '../../../../base/common/event.js';
|
|||
|
||||
export type CodeSelection = { selectionStr: string; filePath: URI }
|
||||
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
type ChatMessage =
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'user';
|
||||
content: string; // content sent to the llm
|
||||
|
|
@ -36,8 +27,21 @@ type ChatMessage =
|
|||
displayContent?: undefined;
|
||||
}
|
||||
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
[id: string]: {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
_currentThreadId: string | null; // intended for internal use only
|
||||
}
|
||||
|
||||
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
|
|
@ -53,24 +57,40 @@ const THREAD_STORAGE_KEY = 'void.threadsHistory'
|
|||
|
||||
export interface IThreadHistoryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
startNewThread(): void;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
|
||||
getCurrentThread(): ChatThreads[string] | null;
|
||||
startNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
startNewThread(): void;
|
||||
addMessageToCurrentThread(message: ChatMessage): void;
|
||||
}
|
||||
|
||||
export const IThreadHistoryService = createDecorator<IThreadHistoryService>('voidThreadHistoryService');
|
||||
class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// the current thread id we are on
|
||||
_currentThreadId: string | null = null
|
||||
|
||||
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
|
||||
private readonly _onDidChangeCurrentThread = new Emitter<void>();
|
||||
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
|
||||
|
||||
state: ThreadsState // allThreads is persisted, currentThread is not
|
||||
|
||||
getAllThreads(): ChatThreads {
|
||||
// storage is the source of truth for threads
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
_currentThreadId: null,
|
||||
allThreads: this._readAllThreads()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
return threads ? JSON.parse(threads) : {}
|
||||
}
|
||||
|
|
@ -79,68 +99,72 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService {
|
|||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...state
|
||||
}
|
||||
if (affectsCurrent) this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
|
||||
getCurrentThread(): ChatThreads[string] | null {
|
||||
const threads = this.getAllThreads()
|
||||
return this._currentThreadId ? threads[this._currentThreadId] ?? null : null
|
||||
return this.state._currentThreadId ? this.state.allThreads[this.state._currentThreadId] ?? null : null;
|
||||
}
|
||||
|
||||
switchToThread(threadId: string) {
|
||||
this._currentThreadId = threadId
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
this._setState({ _currentThreadId: threadId }, true)
|
||||
}
|
||||
|
||||
|
||||
startNewThread() {
|
||||
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const currentThreads = this.getAllThreads()
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId].messages.length === 0) {
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
this._storeAllThreads({
|
||||
|
||||
// update state
|
||||
const newThreads = {
|
||||
...currentThreads,
|
||||
[newThread.id]: newThread
|
||||
})
|
||||
this._currentThreadId = newThread.id
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads, _currentThreadId: newThread.id }, true)
|
||||
}
|
||||
|
||||
|
||||
addMessageToCurrentThread(message: ChatMessage) {
|
||||
let currentThread: ChatThreads[string]
|
||||
const allThreads = this.getAllThreads()
|
||||
const { allThreads, _currentThreadId } = this.state
|
||||
|
||||
if (this._currentThreadId && (this._currentThreadId in allThreads)) {
|
||||
currentThread = allThreads[this._currentThreadId]
|
||||
// get the current thread, or create one
|
||||
let currentThread: ChatThreads[string]
|
||||
if (_currentThreadId && (_currentThreadId in allThreads)) {
|
||||
currentThread = allThreads[_currentThreadId]
|
||||
}
|
||||
else {
|
||||
currentThread = newThreadObject()
|
||||
this._currentThreadId = currentThread.id
|
||||
this.state._currentThreadId = currentThread.id
|
||||
}
|
||||
|
||||
this._storeAllThreads({
|
||||
// update state and store it
|
||||
const newThreads = {
|
||||
...allThreads,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
})
|
||||
|
||||
// the current thread just changed (it had a message added to it)
|
||||
this._onDidChangeCurrentThread.fire()
|
||||
}
|
||||
this._storeAllThreads(newThreads)
|
||||
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);
|
||||
|
|
@ -8,4 +8,4 @@ import './registerSidebar.js'
|
|||
import './registerMetrics.js'
|
||||
|
||||
// register Thread History
|
||||
import './registerThreadsHistory.js'
|
||||
import './registerThreads.js'
|
||||
|
|
|
|||
Loading…
Reference in a new issue