finish converting react to VS

This commit is contained in:
Andrew Pareles 2024-11-11 21:14:43 -08:00
parent 8b9a9c7e2b
commit 6e2030d4d4
28 changed files with 2093 additions and 1341 deletions

326
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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;

View file

@ -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',
// })
})()
// })()

View file

@ -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
// // })
// // )
// }

View file

@ -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;
// }
// }

View file

@ -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;
}

View file

@ -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: [],
};

View file

@ -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).

View file

@ -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>
</>
)
}

View file

@ -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} />
))}
</>
)
}

View file

@ -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;
};

View file

@ -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,
};

View file

@ -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`}>

View file

@ -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>
</>
}

View file

@ -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 (

View file

@ -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>
)
}

View file

@ -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,
// }

View file

@ -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;
} */

View file

@ -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]
}

View file

@ -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;
}
}

View file

@ -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-'

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "NodeNext",
"module": "NodeNext",
"esModuleInterop": true,
},
"include": [
"./src/**/*.ts",
]
}

View file

@ -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);

View 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)
// }
// }

View file

@ -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>;

View file

@ -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);

View file

@ -8,4 +8,4 @@ import './registerSidebar.js'
import './registerMetrics.js'
// register Thread History
import './registerThreadsHistory.js'
import './registerThreads.js'