Compare commits

...

43 Commits

Author SHA1 Message Date
Andrew Pareles 298adac06e fix input box 2024-12-08 17:57:48 -08:00
Andrew Pareles 0f920fb867 remove void Widget inputs for now 2024-12-07 17:20:02 -08:00
Andrew Pareles 4ba00f6216 scope-tailwind doc 2024-12-06 02:55:28 -08:00
Andrew Pareles 2148fe77ec forgot to remove these 2024-12-06 01:33:10 -08:00
Andrew Pareles 1f692ff01a update contributing 2024-12-06 01:22:47 -08:00
Andrew Pareles 8e4f943e30 move node to electron-main, leaving react stuff in contrib/ 2024-12-06 01:03:43 -08:00
Andrew Pareles a52eba91c9 react updates 2024-12-05 22:57:55 -08:00
Andrew Pareles a647530d77 add native input selection in settings 2024-12-05 01:17:05 -08:00
Andrew Pareles 18e3f19512 inputbox ref is now an internal widget (and works better!) 2024-12-04 23:49:45 -08:00
Andrew Pareles 86294c676a fix copy 2024-12-04 23:48:37 -08:00
Andrew Pareles 7cfd9e5580 better msg display 2024-12-04 22:07:25 -08:00
Andrew Pareles d29eca457a fix 0 2024-12-04 21:43:03 -08:00
Andrew Pareles 0855376e02 update message display 2024-12-04 21:36:33 -08:00
Andrew Pareles e1692758c6 ui improvements - abort fix 2024-12-04 20:47:41 -08:00
Andrew Pareles 411167ee44 Merge remote-tracking branch 'origin/main' into ui-improvements 2024-12-04 19:02:47 -08:00
Andrew Pareles 389efb4701 fix scroll 2024-12-04 18:54:05 -08:00
Andrew Pareles eafc3dc0c9 Merge pull request #144 from pythons/feature/llm_provider
refactor: modularize LLM providers and types
2024-12-04 15:12:59 -08:00
Andrew Pareles f2b176dfda remove extensions/ code 2024-12-04 15:12:45 -08:00
Andrew Pareles 02781927e1 Merge branch 'main' into pr/pythons/144 2024-12-04 15:11:42 -08:00
Andrew Pareles 4b3f881e1f rm comment 2024-12-04 15:08:40 -08:00
Andrew Pareles f3653c1c48 clarify config access 2024-12-04 15:07:11 -08:00
Andrew Pareles 3a1955901e move natively 2024-12-04 14:46:36 -08:00
Andrew Pareles e3ce68d2d6 Merge remote-tracking branch 'origin/main' into pr/pythons/144 2024-12-04 14:26:23 -08:00
Andrew Pareles ab3d44387e Merge pull request #141 from Piyu-Pika/Gemini
Add Groq
2024-12-04 14:20:38 -08:00
Andrew Pareles 96e24a89e6 re-remove extension package.json 2024-12-04 14:19:42 -08:00
Andrew Pareles 25cc6aeb3b add readme 2024-12-04 14:18:51 -08:00
Andrew Pareles 4cac92689a integrate into codebase 2024-12-04 14:16:34 -08:00
Piyush Bhardwaj 7444488278 Merge branch 'main' into pr/Piyu-Pika/141 2024-11-27 16:03:59 +05:30
Andrew Pareles 7e22b80986 Merge pull request #161 from voideditor/docs-improvements
Improve documentation
2024-11-26 16:39:46 -08:00
Andrew Pareles 9ec4f3da01 void useful links update 2024-11-26 16:39:01 -08:00
Andrew Pareles 86c9a17f8d improve message 2024-11-26 02:59:16 -08:00
Andrew Pareles d21f2c4d62 fix file capitalization part 2 2024-11-26 02:52:20 -08:00
Andrew Pareles 733256feab fix file capitalization part 1 2024-11-26 02:51:56 -08:00
Andrew Pareles f9ce9de316 Merge pull request #159 from voideditor/fix-cors
Fix CORS + CSP
2024-11-26 02:22:57 -08:00
Piyush Bhardwaj a3e337fe2d Merge branch 'main' into Gemini 2024-11-21 17:51:24 +05:30
Piyush Bhardwaj d12a5f3b38 Merge branch 'main' into Gemini 2024-11-19 18:10:58 +05:30
Piyush Bhardwaj 68b5187f7c Merge branch 'main' into Gemini 2024-11-18 20:13:41 +05:30
Andrew Pareles 5140c79dcf refactor to have openAI conventions 2024-11-17 02:19:21 -08:00
Andrew Pareles 91c65b6060 fix misc 2024-11-17 02:09:39 -08:00
pythons aa6b5c6b6c refactor: modularize LLM providers and types
- Split monolithic sendLLMMessage.ts into separate provider modules
- Create dedicated files for each LLM provider (anthropic, gemini, openai, ollama, greptile)
- Move types into separate types.ts file
- Update import paths across affected files
- No functional changes, purely architectural improvement

This refactoring improves code organization and maintainability by:
- Separating concerns for each LLM provider
- Making the codebase more modular
- Improving type management
2024-11-11 15:10:32 +08:00
Piyush Bhardwaj cc3a08e1ab Merge branch 'main' into Gemini 2024-11-06 22:11:38 +05:30
Piyu-Pika 5b3a6d240e minor bug fix 2024-11-06 22:04:46 +05:30
Piyu-Pika 8897feb290 Groq Added 2024-11-06 12:21:11 +05:30
41 changed files with 1201 additions and 1921 deletions
+2 -12
View File
@@ -13,6 +13,7 @@ There are a few ways to contribute:
Please follow the steps below to build the IDE. If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new) with any build errors, or refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
### a. Build Prerequisites - Mac
@@ -90,15 +91,8 @@ Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin
## Roadmap
Here are the most important topics on our Roadmap. More ⭐'s = more important. Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
## ⭐⭐⭐ Make History work well.
When the user submits a response or presses the apply/accept/reject button, we should add these events to the history, allowing the user to undo/redo them. Right now there is unexpected behavior if the user tries to undo or redo their changes.
## ⭐⭐⭐ Build Cursor-style quick edits (Ctrl+K).
When the user presses Ctrl+K, an input box should appear inline with the code that they were selecting. This is somewhat difficult to do because an extension alone cannot do this, and it requires creating a new component in the IDE. We think you can modify vscode's built-in "codelens" or "zone widget" components, but we are open to alternatives.
## ⭐⭐⭐ Creative.
@@ -106,10 +100,6 @@ Examples: creating better code search, or supporting AI agents that can edit acr
Eventually, we want to build a convenient API for creating AI tools. The API will provide methods for creating the UI (showing an autocomplete suggestion, or creating a new diff), detecting event changes (like `onKeystroke` or `onFileOpen`), and modifying the user's file-system (storing indexes associated with each file), making it much easier to make your own AI plugin. We plan on building these features further along in timeline, but we wanted to list them for completeness.
## ⭐ One-stars.
⭐ Let the user Accept / Reject all Diffs in an entire file via the sidebar.
# Guidelines
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project. Feel free to shoot us a message in the #general channel of the [Discord](https://discord.gg/RSNjgaugJs) for any reason. Please check in especially if you want to make a lot of changes or build a large new feature.
+1 -1
View File
@@ -18,7 +18,7 @@ The Void team put together this list of links to get up and running with VSCode'
## VSCode's Extension API
Void is mainly an extension right now, and these links were very useful for us to get set up.
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
+3
View File
@@ -0,0 +1,3 @@
This folder is no longer relevant, since Void is no longer just an extension.
See the new Void code in `src/vs/workbench/contrib/void` (and a few other locations - you can just search "Void").
@@ -1,471 +0,0 @@
import * as vscode from 'vscode';
import { AbortRef, LLMMessage, sendLLMMessage } from '../common/sendLLMMessage';
import { getVoidConfigFromPartial, VoidConfig } from '../webviews/common/contextForConfig';
import { LRUCache } from 'lru-cache';
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
/*
A summary of autotab:
Postprocessing
-one common problem for all models is outputting unbalanced parentheses
we solve this by trimming all extra closing parentheses from the generated string
in future, should make sure parentheses are always balanced
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
we complete up to first matchup character
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
this should happen automatically with caching system
should break preloaded responses into \n\n chunks
Preprocessing
- we don't generate if cursor is at end / beginning of a line (no spaces)
- we generate 1 line if there is text to the right of cursor
- we generate 1 line if variable declaration
- (in many cases want to show 1 line but generate multiple)
State
- cache based on prefix (and do some trimming first)
- when press tab on one line, should have an immediate followup response
to do this, show autocompletes before they're fully finished
- [todo] remove each autotab when accepted
- [todo] treat windows \r\n separately from \n
!- [todo] provide type information
Details
-generated results are trimmed up to 1 leading/trailing space
-prefixes are cached up to 1 trailing newline
-
*/
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type Autocompletion = {
id: number,
prefix: string,
suffix: string,
startTime: number,
endTime: number | undefined,
abortRef: AbortRef,
status: AutocompletionStatus,
llmPromise: Promise<string> | undefined,
result: string,
}
const DEBOUNCE_TIME = 500
const TIMEOUT_TIME = 60000
const MAX_CACHE_SIZE = 20
const MAX_PENDING_REQUESTS = 2
// postprocesses the result
const postprocessResult = (result: string) => {
console.log('result: ', JSON.stringify(result))
// trim all whitespace except for a single leading/trailing space
const hasLeadingSpace = result.startsWith(' ');
const hasTrailingSpace = result.endsWith(' ');
return (hasLeadingSpace ? ' ' : '')
+ result.trim()
+ (hasTrailingSpace ? ' ' : '');
}
const extractCodeFromResult = (result: string) => {
// extract the code between triple backticks
const parts = result.split(/```(?:\s*\w+)?\n?/);
// if there is no ``` then return the raw result
if (parts.length === 1) {
return result;
}
// else return the code between the triple backticks
return parts[1]
}
// trims the end of the prefix to improve cache hit rate
const trimPrefix = (prefix: string) => {
const trimmedPrefix = prefix.trimEnd()
const trailingEnd = prefix.substring(trimmedPrefix.length)
// keep only a single trailing newline
if (trailingEnd.includes('\n')) {
return trimmedPrefix + '\n'
}
// else ignore all spaces and return the trimmed prefix
return trimmedPrefix
}
function getStringUpToUnbalancedParenthesis(s: string, prefixToTheLeft: string): string {
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
// todo find first open bracket in prefix and get all brackets beyond it in prefix
// get all bracets in prefix
let stack: string[] = []
const firstOpenIdx = prefixToTheLeft.search(/[[({]/);
if (firstOpenIdx !== -1) stack = prefixToTheLeft.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c))
// Iterate through each character
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
else if (char === ')' || char === '}' || char === ']') {
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
}
}
return s;
}
// finds the text in the autocompletion to display, assuming the prefix is already matched
// example:
// originalPrefix = abcd
// generatedMiddle = efgh
// originalSuffix = ijkl
// the user has typed "ef" so prefix = abcdef
// we want to return the rest of the generatedMiddle, which is "gh"
const toInlineCompletion = ({ prefix, suffix, autocompletion, position }: { prefix: string, suffix: string, autocompletion: Autocompletion, position: vscode.Position }): vscode.InlineCompletionItem => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const trimmedOriginalPrefix = trimPrefix(originalPrefix)
const trimmedCurrentPrefix = trimPrefix(prefix)
const suffixLines = suffix.split('\n')
const prefixLines = trimmedCurrentPrefix.split('\n')
const suffixToTheRightOfCursor = suffixLines[0].trim()
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1].trim()
const generatedLines = generatedMiddle.split('\n')
// compute startIdx
let startIdx = trimmedCurrentPrefix.length - trimmedOriginalPrefix.length
if (startIdx < 0) {
return new vscode.InlineCompletionItem('')
}
// compute endIdx
// hacks to get the suffix to render properly with lower quality models
// if the generated text matches with the suffix on the current line, stop
let endIdx: number | undefined = generatedMiddle.length // exclusive bounds
if (suffixToTheRightOfCursor !== '') { // completing in the middle of a line
console.log('1')
// complete until there is a match
const matchIndex = generatedMiddle.lastIndexOf(suffixToTheRightOfCursor[0])
if (matchIndex > 0) { endIdx = matchIndex }
}
if (prefixToTheLeftOfCursor !== '') { // completing the end of a line
console.log('2')
// show a single line
const newlineIdx = generatedMiddle.indexOf('\n')
if (newlineIdx > -1) { endIdx = newlineIdx }
}
// // if a generated line matches with a suffix line, stop
// if (suffixLines.length > 1) {
// console.log('3')
// const lines = []
// for (const generatedLine of generatedLines) {
// if (suffixLines.slice(0, 10).some(suffixLine =>
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
// && generatedLine.trim().startsWith(suffixLine.trim())
// )) break;
// lines.push(generatedLine)
// }
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
// }
let completionStr = generatedMiddle.slice(startIdx, endIdx)
// filter out unbalanced parentheses
console.log('completionStrBeforeParens: ', JSON.stringify(completionStr))
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefixLines.slice(-2).join('\n'))
console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
console.log('finalCompletionStr: ', JSON.stringify(completionStr))
return new vscode.InlineCompletionItem(completionStr, new vscode.Range(position, position))
}
// returns whether this autocompletion is in the cache
const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
const originalPrefix = autocompletion.prefix
const generatedMiddle = autocompletion.result
const originalPrefixTrimmed = trimPrefix(originalPrefix)
const currentPrefixTrimmed = trimPrefix(prefix)
if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
return false
}
const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
return isMatch
}
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
const prefixLines = prefix.split('\n')
const suffixLines = suffix.split('\n')
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToRightOfCursor = suffixLines[0]
// default parameters
let shouldGenerate = true
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
// specific cases
if (suffixToRightOfCursor.trim() !== '') { // typing between something
stopTokens = ['\n', '\r\n']
}
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
// stopTokens = ['\n\n', '\r\n\r\n']
// }
if (prefixToLeftOfCursor === '' || suffixToRightOfCursor === '') { // at beginning or end of line
shouldGenerate = false
}
console.log('shouldGenerate:', shouldGenerate, stopTokens)
return { shouldGenerate, stopTokens }
}
export class AutocompleteProvider implements vscode.InlineCompletionItemProvider {
private _extensionContext: vscode.ExtensionContext;
private _autocompletionId: number = 0;
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
private _lastCompletionTime = 0
private _lastPrefix: string = ''
constructor(context: vscode.ExtensionContext) {
this._extensionContext = context
}
// used internally by vscode
// fires after every keystroke and returns the completion to show
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.InlineCompletionContext,
token: vscode.CancellationToken,
): Promise<vscode.InlineCompletionItem[]> {
const disabled = false
if (disabled) { return []; }
const docUriStr = document.uri.toString()
const fullText = document.getText();
const cursorOffset = document.offsetAt(position);
const prefix = fullText.substring(0, cursorOffset)
const suffix = fullText.substring(cursorOffset)
const voidConfig = getVoidConfigFromPartial(this._extensionContext.globalState.get('partialVoidConfig') ?? {})
// initialize cache and other variables
// note that whenever an autocompletion is rejected, it is removed from cache
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>({
max: MAX_CACHE_SIZE,
dispose: (autocompletion) => {
autocompletion.abortRef.current()
}
})
}
this._lastPrefix = prefix
// get all pending autocompletions
let __c = 0
this._autocompletionsOfDocument[docUriStr].forEach(a => { if (a.status === 'pending') __c += 1 })
console.log('pending: ' + __c)
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
// if the user's change matches up with the generated text
if (doesPrefixMatchAutocompletion({ prefix, autocompletion })) {
cachedAutocompletion = autocompletion
break
}
}
// if there is a cached autocompletion, return it
if (cachedAutocompletion) {
if (cachedAutocompletion.status === 'finished') {
console.log('A1')
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} else if (cachedAutocompletion.status === 'pending') {
console.log('A2')
try {
await cachedAutocompletion.llmPromise;
console.log('id: ' + cachedAutocompletion.id)
const inlineCompletion = toInlineCompletion({ autocompletion: cachedAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} catch (e) {
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
console.error('Error creating autocompletion (1): ' + e)
}
} else if (cachedAutocompletion.status === 'error') {
console.log('A3')
}
return []
}
// else if no more typing happens, then go forwards with the request
// wait DEBOUNCE_TIME for the user to stop typing
const thisTime = Date.now()
this._lastCompletionTime = thisTime
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
setTimeout(() => {
if (this._lastCompletionTime === thisTime) {
resolve(false)
} else {
resolve(true)
}
}, DEBOUNCE_TIME)
)
// if more typing happened, then do not go forwards with the request
if (didTypingHappenDuringDebounce) {
return []
}
console.log('B')
// if there are too many pending requests, cancel the oldest one
let numPending = 0
let oldestPending: Autocompletion | undefined = undefined
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].values()) {
if (autocompletion.status === 'pending') {
numPending += 1
if (oldestPending === undefined) {
oldestPending = autocompletion
}
if (numPending >= MAX_PENDING_REQUESTS) {
// cancel the oldest pending request and remove it from cache
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
break
}
}
}
const { shouldGenerate, stopTokens } = getCompletionOptions({ prefix, suffix })
if (!shouldGenerate) return []
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
prefix: prefix,
suffix: suffix,
startTime: Date.now(),
endTime: undefined,
abortRef: { current: () => { } },
status: 'pending',
llmPromise: undefined,
result: '',
}
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
sendLLMMessage({
mode: 'fim',
fimInfo: { prefix, suffix },
options: { stopTokens },
onText: async (tokenStr, completionStr) => {
newAutocompletion.result = completionStr
// if generation doesn't match the prefix for the first few tokens generated, reject it
if (!doesPrefixMatchAutocompletion({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
reject('LLM response did not match user\'s text.')
}
},
onFinalMessage: (finalMessage) => {
// newAutocompletion.prefix = prefix
// newAutocompletion.suffix = suffix
// newAutocompletion.startTime = Date.now()
newAutocompletion.endTime = Date.now()
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
newAutocompletion.result = postprocessResult(extractCodeFromResult(finalMessage))
resolve(newAutocompletion.result)
},
onError: (e) => {
newAutocompletion.endTime = Date.now()
newAutocompletion.status = 'error'
reject(e)
},
voidConfig,
abortRef: newAutocompletion.abortRef,
})
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
setTimeout(() => {
if (newAutocompletion.status === 'pending') {
reject('Timeout receiving message to LLM.')
}
}, TIMEOUT_TIME)
})
// add autocompletion to cache
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
// show autocompletion
try {
await newAutocompletion.llmPromise
console.log('id: ' + newAutocompletion.id)
const inlineCompletion = toInlineCompletion({ autocompletion: newAutocompletion, prefix, suffix, position })
return [inlineCompletion]
} catch (e) {
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
console.error('Error creating autocompletion (2): ' + e)
return []
}
}
}
+164 -76
View File
@@ -13,6 +13,8 @@
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@parcel/watcher": "2.1.0",
"@rrweb/record": "^2.0.0-alpha.17",
"@rrweb/types": "^2.0.0-alpha.17",
"@vscode/deviceid": "^0.1.1",
"@vscode/iconv-lite-umd": "0.7.0",
"@vscode/policy-watcher": "^1.1.4",
@@ -35,6 +37,7 @@
"@xterm/addon-webgl": "^0.19.0-beta.64",
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"groq-sdk": "^0.9.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.3",
@@ -46,6 +49,7 @@
"native-watchdog": "^1.4.1",
"node-pty": "1.1.0-beta21",
"open": "^8.4.2",
"posthog-node": "^4.3.1",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-oniguruma": "1.7.0",
@@ -155,7 +159,6 @@
"path-browserify": "^1.0.1",
"postcss": "^8.4.33",
"postcss-nesting": "^12.0.2",
"posthog-js": "^1.184.2",
"pump": "^1.0.1",
"rcedit": "^1.1.0",
"react": "^18.3.1",
@@ -2598,6 +2601,31 @@
"win32"
]
},
"node_modules/@rrweb/record": {
"version": "2.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/@rrweb/record/-/record-2.0.0-alpha.17.tgz",
"integrity": "sha512-Je+lzjeWMF8/I0IDoXFzkGPKT8j7AkaBup5YcwUHlkp18VhLVze416MvI6915teE27uUA2ScXMXzG0Yiu5VTIw==",
"license": "MIT",
"dependencies": {
"@rrweb/types": "^2.0.0-alpha.17",
"rrweb": "^2.0.0-alpha.17"
}
},
"node_modules/@rrweb/types": {
"version": "2.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.17.tgz",
"integrity": "sha512-AfDTVUuCyCaIG0lTSqYtrZqJX39ZEYzs4fYKnexhQ+id+kbZIpIJtaut5cto6dWZbB3SEe4fW0o90Po3LvTmfg==",
"license": "MIT",
"dependencies": {
"rrweb-snapshot": "^2.0.0-alpha.17"
}
},
"node_modules/@rrweb/utils": {
"version": "2.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/@rrweb/utils/-/utils-2.0.0-alpha.17.tgz",
"integrity": "sha512-HCsasPERBwOS9/LQeOytO2ETKTCqRj1wORBuxiy3t41hKhmi225DdrUPiWnyDdTQm1GdVbOymMRknJVPnZaSXw==",
"license": "MIT"
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -2971,6 +2999,12 @@
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==",
"dev": true
},
"node_modules/@types/css-font-loading-module": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz",
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
"license": "MIT"
},
"node_modules/@types/debug": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz",
@@ -3103,7 +3137,6 @@
"version": "20.14.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz",
"integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -3112,7 +3145,6 @@
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -4424,6 +4456,12 @@
}
}
},
"node_modules/@xstate/fsm": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz",
"integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==",
"license": "MIT"
},
"node_modules/@xterm/addon-clipboard": {
"version": "0.2.0-beta.47",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.47.tgz",
@@ -4507,7 +4545,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dev": true,
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
@@ -4588,7 +4625,6 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"dev": true,
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
@@ -5143,8 +5179,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/atob": {
"version": "2.1.2",
@@ -5212,6 +5247,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
@@ -5320,6 +5366,15 @@
"node": ">=0.10.0"
}
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -6288,7 +6343,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -6300,7 +6354,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -6578,18 +6631,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/core-js": {
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -8070,7 +8111,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -8490,13 +8530,6 @@
}
}
},
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"dev": true,
"license": "MIT"
},
"node_modules/figures": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
@@ -8899,6 +8932,26 @@
"util-deprecate": "~1.0.1"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -8949,7 +9002,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -8963,7 +9015,6 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"dev": true,
"license": "MIT"
},
"node_modules/format": {
@@ -8979,7 +9030,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"node-domexception": "1.0.0",
@@ -9893,6 +9943,30 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/groq-sdk": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.9.0.tgz",
"integrity": "sha512-Kdbl65yOwd5ga+UZd3KCULnoOlcPV7qJ+FfemivVpu3bOZeT7WJWh/ywAuivwJ7aJzcXmwf6x5t9x0MV0ZmR0A==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
}
},
"node_modules/groq-sdk/node_modules/@types/node": {
"version": "18.19.67",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz",
"integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/gulp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
@@ -12140,7 +12214,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
@@ -14715,7 +14788,6 @@
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -14724,7 +14796,6 @@
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"dev": true,
"dependencies": {
"mime-db": "1.45.0"
},
@@ -14806,6 +14877,12 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mixin-deep": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
@@ -15339,7 +15416,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -15359,7 +15435,6 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz",
"integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -16570,7 +16645,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -16816,7 +16890,6 @@
"version": "8.4.48",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.48.tgz",
"integrity": "sha512-GCRK8F6+Dl7xYniR5a4FYbpBzU8XnZVeowqsQFYdcXuSbChgiks7qybSkbvnaeqv0G0B+dd9/jJgH8kkLDQeEA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -17550,7 +17623,6 @@
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
@@ -17564,28 +17636,17 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/posthog-js": {
"version": "1.184.2",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.184.2.tgz",
"integrity": "sha512-n7OxFntH7m4sw/GsuUAliWGqBDV7g7IemvjwGISCQFQpf5cSqZgmvT2V2O3GHXpONiEgwL/Ud/+zWmhZbWLExg==",
"dev": true,
"node_modules/posthog-node": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.3.1.tgz",
"integrity": "sha512-By9SEGZxBLC7GVyVb+HlJlpxM/xI4iLUgwtsBS8f4bZ0wqYKiNHoYcFEwMJnTM9xQcQZztr6dqLmsJ7jCv0vxA==",
"license": "MIT",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.0"
}
},
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
"axios": "^1.7.4",
"rusha": "^0.8.14"
},
"engines": {
"node": ">=15.0.0"
}
},
"node_modules/prebuild-install": {
@@ -18667,6 +18728,40 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rrdom": {
"version": "2.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.0.0-alpha.17.tgz",
"integrity": "sha512-b6caDiNcFO96Opp7TGdcVd4OLGSXu5dJe+A0IDiAu8mk7OmhqZCSDlgQdTKmdO5wMf4zPsUTgb8H/aNvR3kDHA==",
"license": "MIT",
"dependencies": {
"rrweb-snapshot": "^2.0.0-alpha.17"
}
},
"node_modules/rrweb": {
"version": "2.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.0.0-alpha.17.tgz",
"integrity": "sha512-GQxBkCC4r9XL2bwSdv7iIS49M3cEA8OtObVq0rrQ4GUT4+h7omucGQ4x7m5YN5Vq1oalStBaBlYqF7yRnfG3JA==",
"license": "MIT",
"dependencies": {
"@rrweb/types": "^2.0.0-alpha.17",
"@rrweb/utils": "^2.0.0-alpha.17",
"@types/css-font-loading-module": "0.0.7",
"@xstate/fsm": "^1.4.0",
"base64-arraybuffer": "^1.0.1",
"mitt": "^3.0.0",
"rrdom": "^2.0.0-alpha.17",
"rrweb-snapshot": "^2.0.0-alpha.17"
}
},
"node_modules/rrweb-snapshot": {
"version": "2.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.17.tgz",
"integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==",
"license": "MIT",
"dependencies": {
"postcss": "^8.4.38"
}
},
"node_modules/run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@@ -18696,6 +18791,12 @@
}
]
},
"node_modules/rusha": {
"version": "0.8.14",
"resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz",
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==",
"license": "MIT"
},
"node_modules/rxjs": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
@@ -19594,7 +19695,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -21199,8 +21299,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tree-kill": {
"version": "1.2.2",
@@ -21794,8 +21893,7 @@
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/union-value": {
"version": "1.0.1",
@@ -22269,7 +22367,6 @@
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -22281,18 +22378,10 @@
"integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==",
"dev": true
},
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack": {
"version": "5.94.0",
@@ -22560,7 +22649,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
+4 -1
View File
@@ -75,6 +75,8 @@
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@parcel/watcher": "2.1.0",
"@rrweb/record": "^2.0.0-alpha.17",
"@rrweb/types": "^2.0.0-alpha.17",
"@vscode/deviceid": "^0.1.1",
"@vscode/iconv-lite-umd": "0.7.0",
"@vscode/policy-watcher": "^1.1.4",
@@ -97,6 +99,7 @@
"@xterm/addon-webgl": "^0.19.0-beta.64",
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"groq-sdk": "^0.9.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.3",
@@ -108,6 +111,7 @@
"native-watchdog": "^1.4.1",
"node-pty": "1.1.0-beta21",
"open": "^8.4.2",
"posthog-node": "^4.3.1",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-oniguruma": "1.7.0",
@@ -217,7 +221,6 @@
"path-browserify": "^1.0.1",
"postcss": "^8.4.33",
"postcss-nesting": "^12.0.2",
"posthog-js": "^1.184.2",
"pump": "^1.0.1",
"rcedit": "^1.1.0",
"react": "^18.3.1",
+2 -2
View File
@@ -52,10 +52,10 @@
"./typings",
"./vs/**/*.ts",
"vscode-dts/vscode.proposed.*.d.ts",
"vscode-dts/vscode.d.ts"
"vscode-dts/vscode.d.ts",
// Void added these:
// "./vs/**/*.tsx",
// "./vs/workbench/contrib/void/browser/**.tsx",
// "./vs/**/*.d.mts",
]
}
+9 -2
View File
@@ -122,6 +122,8 @@ import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cs
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../platform/void/common/metricsService.js';
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
/**
* The main VS Code application. There will only ever be one instance,
@@ -1103,6 +1105,9 @@ export class CodeApplication extends Disposable {
services.set(ITelemetryService, NullTelemetryService);
}
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
// Default Extensions Profile Init
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
@@ -1238,8 +1243,10 @@ export class CodeApplication extends Disposable {
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
// Void
// const sendLLMMessageChannel = ProxyChannel.fromService(accessor.get(ISendLLMMessageService), disposables);
const sendLLMMessageChannel = new LLMMessageChannel();
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel);
// Extension Host Debug Broadcasting
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { IMetricsService } from '../common/metricsService.js';
// BROWSER IMPLEMENTATION, calls channel
export class MetricsService implements IMetricsService {
readonly _serviceBrand: undefined;
private readonly channel: IChannel;
constructor(
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
) {
this.channel = mainProcessService.getChannel('void-channel-metrics')
}
// call capture on the channel
capture(...params: Parameters<IMetricsService['capture']>) {
this.channel.call('capture', params);
}
}
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
+210
View File
@@ -0,0 +1,210 @@
const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
return {
description,
defaultVal,
enumArr,
}
}
const configString = (description: string, defaultVal: string) => {
return {
description,
defaultVal,
enumArr: undefined,
}
}
// fields you can customize (don't forget 'default' - it isn't included here!)
export const nonDefaultConfigFields = [
'anthropic',
'openAI',
'gemini',
'greptile',
'groq',
'ollama',
'openRouter',
'openAICompatible',
'azure',
] as const
export const voidConfigInfo: Record<
typeof nonDefaultConfigFields[number] | 'default', {
[prop: string]: {
description: string;
enumArr?: readonly string[] | undefined;
defaultVal: string;
};
}
> = {
default: {
whichApi: configEnum(
'API Provider.',
'anthropic',
nonDefaultConfigFields,
),
maxTokens: configEnum(
'Max number of tokens to output.',
'1024',
[
'default', // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
'1024',
'2048',
'4096',
'8192'
] as const,
),
},
anthropic: {
apikey: configString('Anthropic API key.', ''),
model: configEnum(
'Anthropic model to use.',
'claude-3-5-sonnet-20240620',
[
'claude-3-5-sonnet-20240620',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
] as const,
),
},
openAI: {
apikey: configString('OpenAI API key.', ''),
model: configEnum(
'OpenAI model to use.',
'gpt-4o',
[
'o1-preview',
'o1-mini',
'gpt-4o',
'gpt-4o-2024-05-13',
'gpt-4o-2024-08-06',
'gpt-4o-mini',
'gpt-4o-mini-2024-07-18',
'gpt-4-turbo',
'gpt-4-turbo-2024-04-09',
'gpt-4-turbo-preview',
'gpt-4-0125-preview',
'gpt-4-1106-preview',
'gpt-4',
'gpt-4-0613',
'gpt-3.5-turbo-0125',
'gpt-3.5-turbo',
'gpt-3.5-turbo-1106'
] as const
),
},
greptile: {
apikey: configString('Greptile API key.', ''),
githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
remote: configEnum(
'Repo location',
'github',
[
'github',
'gitlab'
] as const
),
repository: configString('Repository identifier in "owner / repository" format.', ''),
branch: configString('Name of the branch to use.', 'main'),
},
groq: {
apikey: configString('Groq API key.', ''),
model: configEnum(
'Groq model to use.',
'mixtral-8x7b-32768',
[
"mixtral-8x7b-32768",
"llama2-70b-4096",
"gemma-7b-it"
] as const
),
},
ollama: {
endpoint: configString(
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode - webview://*" ollama serve`.',
'http://127.0.0.1:11434'
),
model: configEnum(
'Ollama model to use.',
'codestral',
['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b'] as const
),
},
openRouter: {
model: configString(
'OpenRouter model to use.',
'openai/gpt-4o'
),
apikey: configString('OpenRouter API key.', ''),
},
openAICompatible: {
endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'),
model: configString('The name of the model to use.', 'gpt-4o'),
apikey: configString('Your API key.', ''),
},
azure: {
// 'void.azure.apiKey': {
// 'type': 'string',
// 'description': 'Azure API key.'
// },
// 'void.azure.deploymentId': {
// 'type': 'string',
// 'description': 'Azure API deployment ID.'
// },
// 'void.azure.resourceName': {
// 'type': 'string',
// 'description': 'Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`'
// },
// 'void.azure.providerSettings': {
// 'type': 'object',
// 'properties': {
// 'baseURL': {
// 'type': 'string',
// 'default': 'https://${resourceName}.openai.azure.com/openai/deployments',
// 'description': 'Azure API base URL.'
// },
// 'headers': {
// 'type': 'object',
// 'description': 'Custom headers to include in the requests.'
// }
// }
// },
},
gemini: {
apikey: configString('Google API key.', ''),
model: configEnum(
'Gemini model to use.',
'gemini-1.5-flash',
[
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
] as const
),
},
}
// this is the type that comes with metadata like desc, default val, etc
export type VoidConfigInfo = typeof voidConfigInfo
export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number]
// this is the type that specifies the user's actual config
export type PartialVoidConfig = {
[K in keyof typeof voidConfigInfo]?: {
[P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal']
}
}
export type VoidConfig = {
[K in keyof typeof voidConfigInfo]: {
[P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal']
}
}
@@ -3,7 +3,8 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { VoidConfig } from '../../../workbench/contrib/void/browser/registerConfig.js';
import { VoidConfig } from './configTypes.js'
// ---------- type definitions ----------
@@ -0,0 +1,10 @@
import { createDecorator } from '../../instantiation/common/instantiation.js';
export interface IMetricsService {
readonly _serviceBrand: undefined;
capture(event: string, params: Record<string, any>): void;
}
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
@@ -0,0 +1,57 @@
import Anthropic from '@anthropic-ai/sdk';
import { parseMaxTokensStr, SendLLMMessageFnTypeInternal } from './util.js';
// Anthropic
type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const thisConfig = voidConfig.anthropic
const anthropic = new Anthropic({ apiKey: thisConfig.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: thisConfig.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({ fullText: 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({ error: 'Invalid API key.' })
}
else {
onError({ error })
}
})
// TODO need to test this to make sure it works, it might throw an error
_setAborter(() => stream.controller.abort())
};
@@ -0,0 +1,48 @@
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { SendLLMMessageFnTypeInternal } from './util';
// Gemini
export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
const thisConfig = voidConfig.gemini
const genAI = new GoogleGenerativeAI(thisConfig.apikey);
const model = genAI.getGenerativeModel({ model: thisConfig.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 && error.status === 400) {
onError({ error: 'Invalid API key.' });
}
else {
onError({ error });
}
})
}
@@ -0,0 +1,64 @@
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
import { SendLLMMessageFnTypeInternal } from './util';
export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
const thisConfig = voidConfig.greptile
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
'Authorization': `Bearer ${thisConfig.apikey}`,
'X-Github-Token': `${thisConfig.githubPAT}`,
'Content-Type': `application/json`,
},
body: JSON.stringify({
messages,
stream: true,
repositories: [thisConfig.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({ newText: message, fullText })
}
else if (type === 'sources') {
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
fullText += filepath
onText({ newText: filepath, fullText })
}
// type: 'status' with an empty 'message' means last message
else if (type === 'status') {
if (!message) {
onFinalMessage({ fullText })
}
}
}
})
.catch(error => {
onError({ error })
});
}
@@ -0,0 +1,40 @@
import Groq from 'groq-sdk';
import { SendLLMMessageFnTypeInternal } from './util';
import { parseMaxTokensStr } from './util.js';
// Groq
export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = '';
const groq = new Groq({
apiKey: voidConfig.groq.apikey,
dangerouslyAllowBrowser: true
});
await groq.chat.completions
.create({
messages: messages,
model: voidConfig.groq.model,
stream: true,
temperature: 0.7,
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens),
})
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
if (newText) {
fullText += newText;
onText({ newText, fullText });
}
}
onFinalMessage({ fullText });
})
.catch(error => {
onError({ error });
})
};
@@ -0,0 +1,47 @@
import { Ollama, ErrorResponse } from 'ollama';
import { SendLLMMessageFnTypeInternal } from './util';
import { parseMaxTokensStr } from './util.js';
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const thisConfig = voidConfig.ollama
let fullText = ''
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.chat({
model: thisConfig.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) => {
if (typeof error === 'object') {
const e = error.error as ErrorResponse['error']
if (e) {
const name = error.name ?? 'Error'
onError({ error: `${name}: ${e}` })
return;
}
}
onError({ error })
})
};
@@ -0,0 +1,64 @@
import OpenAI from 'openai';
import { SendLLMMessageFnTypeInternal } from './util';
import { parseMaxTokensStr } from './util.js';
// OpenAI, OpenRouter, OpenAICompatible
export 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') {
const thisConfig = voidConfig.openAI
openai = new OpenAI({ apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true });
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openRouter') {
const thisConfig = voidConfig.openRouter
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.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: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openAICompatible') {
const thisConfig = voidConfig.openAICompatible
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true })
options = { model: thisConfig.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 && error.status === 401) {
onError({ error: 'Invalid API key.' });
}
else {
onError({ error });
}
})
};
@@ -0,0 +1,109 @@
import { SendLLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicMsg } from './anthropic.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGreptileMsg } from './greptile.js';
import { sendGroqMsg } from './groq.js';
import { sendOllamaMsg } from './ollama.js';
import { sendOpenAIMsg } from './openai.js';
export const sendLLMMessage = (
{
messages, onText: onText_, onFinalMessage: onFinalMessage_, onError: onError_,
abortRef: abortRef_, voidConfig, logging: { loggingName },
}: SendLLMMMessageParams,
metricsService: IMetricsService
) => {
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) => {
metricsService.capture(eventId, {
whichApi: voidConfig.default['whichApi'],
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
version: '2024-11-14',
...extras,
})
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText: OnText = ({ newText, fullText }) => {
if (_didAbort) return
onText_({ newText, fullText })
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText })
}
const onError: OnError = ({ error }) => {
if (_didAbort) return
console.error('sendLLMMessage onError:', error)
captureChatEvent(`${loggingName} - Error`, { error })
onError_({ error })
}
const onAbort = () => {
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
try { _aborter?.() } // aborter sometimes automatically throws an error
catch (e) { }
_didAbort = true
}
abortRef_.current = onAbort
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
try {
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;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
default:
onError({ error: `Error: whichApi was "${voidConfig.default.whichApi}", which is not recognized!` })
break;
}
}
catch (error) {
if (error instanceof Error) { onError({ error }) }
else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); }
// ; (_aborter as any)?.()
// _didAbort = true
}
}
@@ -0,0 +1,21 @@
import { VoidConfig } from '../../common/configTypes'
import { LLMMessage, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes'
export 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
}
export type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
voidConfig: VoidConfig;
_setAborter: (aborter: () => void) => void;
}) => void
@@ -3,17 +3,16 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
// this channel is registered in `app.ts`
// code convention is to make a service responsible for this stuff, and not a channel, but this is simpler.
// you could create one instance in electron-main/my-service.ts and one in browser/my-service.ts (and define the interface IMyService in common/my-service.ts), but we just use a channel here
// registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
// registered in app.ts
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { sendLLMMessage } from '../../../workbench/contrib/void/browser/react/out/util/sendLLMMessage.js';
import { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
// NODE IMPLEMENTATION OF SENDLLMMESSAGE - calls sendLLMMessage() and returns listeners
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
export class LLMMessageChannel implements IServerChannel {
private readonly _onText = new Emitter<ProxyOnTextPayload>();
@@ -29,7 +28,9 @@ export class LLMMessageChannel implements IServerChannel {
private readonly _abortRefOfRequestId: Record<string, AbortRef> = {}
constructor() { }
constructor(
private readonly metricsService: IMetricsService
) { }
// browser uses this to listen for changes
listen(_: unknown, event: typeof listenerNames[number]): Event<any> {
@@ -80,7 +81,7 @@ export class LLMMessageChannel implements IServerChannel {
onError: ({ error }) => { this._onError.fire({ requestId, error }); },
abortRef: this._abortRefOfRequestId[requestId],
}
sendLLMMessage(mainThreadParams);
sendLLMMessage(mainThreadParams, this.metricsService);
}
private _callAbort(params: ProxyLLMMessageAbortParams) {
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node'
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
// const buildEnv = 'development';
// const buildNumber = '1.0.0';
// const isMac = process.platform === 'darwin';
// // TODO use commandKey
// const commandKey = isMac ? '⌘' : 'Ctrl';
// const systemInfo = {
// buildEnv,
// buildNumber,
// isMac,
// }
export class MetricsMainService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
readonly _distinctId: string
readonly client: PostHog
constructor(
@ITelemetryService private readonly _telemetryService: ITelemetryService
) {
super()
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
this._distinctId = devDeviceId
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
}
capture: IMetricsService['capture'] = (event, params) => {
console.log('Capturing', { event, params })
console.log('full capture:', { distinctId: this._distinctId, event, properties: params })
this.client.capture({ distinctId: this._distinctId, event, properties: params })
}
}
@@ -3,11 +3,11 @@ import { execSync } from 'child_process';
// clear temp dirs
execSync('npx rimraf out/ && npx rimraf src2/')
// build and scope tailwind
// build and scope tailwind: https://www.npmjs.com/package/scope-tailwind
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "prefix-" ')
// tsup to build src2/ into out/
execSync('npx tsup')
console.log('✅ Done building! Press Cmd+Shift+B again.')
console.log('✅ Done building! Kill your build script(s) (Ctrl+D in them), then press Cmd+Shift+B again.')
@@ -16,6 +16,9 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
const inlineDiffService = useService('inlineDiffService')
const clipboardService = useService('clipboardService')
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
@@ -25,15 +28,10 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
clipboardService.writeText(text)
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
}, [text, clipboardService])
return <>
<button
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { getCmdKey } from '../../../getCmdKey.js';
// const opaqueMessage = `\
// Unfortunately, Void can't see the full error. However, you should be able to find more details by pressing ${getCmdKey()}+Shift+P, typing "Toggle Developer Tools", and looking at the console.\n
@@ -34,7 +33,8 @@ const getErrorDetails = (error: unknown) => {
}
// sometimes error is an object but not an Error
else if (typeof error === 'object') {
e = new Error('More details below.', { cause: JSON.stringify(error) })
e = new Error(`More details below.`, { cause: JSON.stringify(error) })
}
else {
e = new Error(String(error))
@@ -68,7 +68,6 @@ export const ErrorDisplay = ({
error,
onDismiss = null,
showDismiss = true,
className = ''
}: {
error: Error | object | string,
onDismiss: (() => void) | null,
@@ -81,7 +80,7 @@ export const ErrorDisplay = ({
const hasDetails = details.cause || Object.keys(details.additional).length > 0;
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 ${className}`}>
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex gap-3">
@@ -18,8 +18,9 @@ const Sidebar = () => {
const sidebarState = useSidebarState()
const { isHistoryOpen, currentTab: tab } = sidebarState
// className='@@void-scope'
return <div className='@@void-scope'>
<div className={`flex flex-col h-screen w-full`}>
<div className={`flex flex-col w-full px-2 py-2`}>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
@@ -27,7 +28,7 @@ const Sidebar = () => {
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
<div className={`mb-2 h-[30vh] ${isHistoryOpen ? '' : 'hidden'}`}>
<div className={`mb-2 ${isHistoryOpen ? '' : 'hidden'}`}>
<SidebarThreadSelector />
</div>
@@ -42,6 +43,7 @@ const Sidebar = () => {
</div>
</div>
}
@@ -8,7 +8,7 @@ import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState }
import { useConfigState, useService, useThreadsState } from '../util/services.js';
import { generateDiffInstructions } from '../../../prompt/systemPrompts.js';
import { userInstructionsStr } from '../../../prompt/stringifyFiles.js';
import { CodeSelection, CodeStagingSelection } from '../../../registerThreads.js';
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../registerThreads.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { MarkdownRender } from '../markdown/MarkdownRender.js';
@@ -16,10 +16,11 @@ import { IModelService } from '../../../../../../../editor/common/services/model
import { URI } from '../../../../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from '../util/ErrorDisplay.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
// import { } from '@vscode/webview-ui-toolkit/react';
import { getCmdKey } from '../../../getCmdKey.js'
import { HistoryInputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox } from './inputs.js';
// read files from VSCode
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
@@ -29,27 +30,6 @@ const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string
}
export type ChatMessage =
| {
role: 'user';
content: string; // content sent to the llm
displayContent: string; // content displayed to user
selections: CodeSelection[] | null; // the user's selection
}
| {
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 /
@@ -169,11 +149,9 @@ export const SidebarChat = () => {
const threadsStateService = useService('threadsStateService')
// ----- SIDEBAR CHAT state (local) -----
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
// state of chat
const [messageStream, setMessageStream] = useState('')
const [messageStream, setMessageStream] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const latestRequestIdRef = useRef<string | null>(null)
@@ -181,9 +159,14 @@ export const SidebarChat = () => {
const sendLLMMessageService = useService('sendLLMMessageService')
const isDisabled = !instructions
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
const isDisabled = !instructions
const formRef = useRef<HTMLFormElement | null>(null)
const inputBoxRef: React.MutableRefObject<HistoryInputBox | null> = useRef(null);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
@@ -203,8 +186,8 @@ export const SidebarChat = () => {
threadsStateService.addMessageToCurrentThread(systemPromptElt)
const userContent = userInstructionsStr(instructions, selections)
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selections }
threadsStateService.addMessageToCurrentThread(newHistoryElt)
const userHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selections }
threadsStateService.addMessageToCurrentThread(userHistoryElt)
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
@@ -213,24 +196,24 @@ export const SidebarChat = () => {
const object: LLMMessageServiceParams = {
logging: { loggingName: 'Chat' },
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content || '(null)' })),],
onText: ({ newText, fullText }) => setMessageStream(fullText),
onFinalMessage: ({ fullText: content }) => {
console.log('chat: running final message')
// add assistant's message to chat history, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
threadsStateService.addMessageToCurrentThread(newHistoryElt)
setMessageStream('')
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream(null)
setIsLoading(false)
},
onError: ({ error }) => {
console.log('chat: running error', 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)
let content = messageStream ?? ''; // just use the current content
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null, }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream('')
setIsLoading(false)
@@ -245,8 +228,10 @@ export const SidebarChat = () => {
setIsLoading(true)
setInstructions('');
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
if (inputBoxRef.current) {
inputBoxRef.current.value = ''; // this triggers onDidChangeText
inputBoxRef.current.blur();
}
threadsStateService.setStaging([]) // clear staging
setLatestError(null)
@@ -258,9 +243,9 @@ export const SidebarChat = () => {
sendLLMMessageService.abort(latestRequestIdRef.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)
const llmContent = messageStream ?? ''
const assistantHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream || null, }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream('')
setIsLoading(false)
@@ -279,7 +264,7 @@ export const SidebarChat = () => {
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} />
</div>
{/* chatbar */}
<div className="shrink-0 py-4">
@@ -292,31 +277,47 @@ export const SidebarChat = () => {
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
</div>}
{/* error message */}
{latestError === null ? null :
<ErrorDisplay
error={latestError}
onDismiss={() => { setLatestError(null) }}
/>}
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2"
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
{/* input */}
<VoidInputBox
placeholder={`${getCmdKey()}+L to select`}
onChangeText={onChangeText}
inputBoxRef={inputBoxRef}
multiline={true}
initVal=''
/>
{/* <textarea
ref={chatInputRef}
placeholder={`Press ${getCmdKey()}+L to select.`}
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"
className={`w-full p-2 leading-tight resize-none max-h-[50vh] overflow-auto bg-transparent border-none !outline-none`}
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"
className="font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
>
<svg
className='scale-50'
@@ -327,7 +328,7 @@ export const SidebarChat = () => {
:
// 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"
className="font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={isDisabled}
type='submit'
>
@@ -341,13 +342,6 @@ export const SidebarChat = () => {
</div>
</div>
</div>
{/* error message */}
{latestError === null ? null :
<ErrorDisplay
error={latestError}
onDismiss={() => { setLatestError(null) }}
/>}
</div>
</>
}
@@ -2,9 +2,13 @@
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useConfigState, useService } from '../util/services.js';
import { IVoidConfigStateService, nonDefaultConfigFields, PartialVoidConfig, VoidConfig, VoidConfigField, VoidConfigInfo, SetFieldFnType, ConfigState } from '../../../registerConfig.js';
import { HistoryInputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
import { ConfigState, IVoidConfigStateService } from '../../../registerConfig.js';
import { nonDefaultConfigFields, VoidConfigField } from '../../../../../../../platform/void/common/configTypes.js';
const SettingOfFieldAndParam = ({ field, param, configState, configStateService }:
@@ -15,14 +19,31 @@ const SettingOfFieldAndParam = ({ field, param, configState, configStateService
const { enumArr, defaultVal, description } = configStateService.voidConfigInfo[field][param]
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
const initValRef = useRef(val)
const updateState = useCallback((newValue: string) => {
configStateService.setField(field, param, newValue)
}, [configStateService, field, param])
const inputBoxRef = useRef<HistoryInputBox | null>(null);
const selectBoxRef = useRef<SelectBox | null>(null);
const forceState = useCallback((newValue: string) => {
if (inputBoxRef.current) {
inputBoxRef.current.value = newValue;
}
if (selectBoxRef.current) {
selectBoxRef.current.select(enumArr?.indexOf(newValue) ?? 0);
}
// updateState is called automatically when the change happens
}, [enumArr, updateState])
const updateState = (newValue: string) => { configStateService.setField(field, param, newValue) }
const resetButton = <button
disabled={val === defaultVal}
title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
onClick={() => updateState(defaultVal)}
onClick={() => forceState(defaultVal)}
>
<svg
className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
@@ -30,16 +51,30 @@ const SettingOfFieldAndParam = ({ field, param, configState, configStateService
</svg>
</button>
const inputElement = enumArr === undefined ?
// string
(<input
// (<VoidInputBox
// onChangeText={updateState}
// initVal={initValRef.current}
// multiline={false}
// placeholder=''
// inputBoxRef={inputBoxRef}
// />)
<input
className='input p-1 w-full'
type='text'
value={val}
onChange={(e) => updateState(e.target.value)}
/>)
/>
:
// enum
// (<VoidSelectBox
// onChangeSelection={updateState}
// initVal={initValRef.current}
// options={enumArr}
// selectBoxRef={selectBoxRef}
// />)
(<select
className='dropdown p-1 w-full'
value={val}
@@ -23,10 +23,10 @@ export const SidebarThreadSelector = () => {
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)
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">
<div className="flex flex-col gap-y-1 overflow-y-auto h-[30vh]">
{/* X button at top right */}
<div className="text-right">
@@ -56,21 +56,26 @@ export const SidebarThreadSelector = () => {
let btnStringArr: string[] = []
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
btnStringArr.push(msg1)
const firstMsgIdx = allThreads[threadId].messages.findIndex(msg => msg.role !== 'system' && !!msg.displayContent) ?? ''
if (firstMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[firstMsgIdx].displayContent ?? ''))
else
btnStringArr.push('""')
let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
if (msg2)
btnStringArr.push(msg2)
const secondMsgIdx = allThreads[threadId].messages.findIndex((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx) ?? ''
if (secondMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[secondMsgIdx].displayContent ?? ''))
btnStringArr.push(allThreads[threadId].messages.length + '')
const numMessagesRemaining = allThreads[threadId].messages.filter((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > secondMsgIdx).length
if (numMessagesRemaining > 0)
btnStringArr.push(numMessagesRemaining + '')
const btnString = btnStringArr.join(' / ')
return (
<button
key={pastThread.id}
className={`btn btn-sm rounded-sm ${pastThread.id === threadsStateService.getCurrentThread(threadsState)?.id ? "btn-primary" : "btn-secondary"}`}
className={`rounded-sm`}
onClick={() => threadsStateService.switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
@@ -0,0 +1,104 @@
import React, { useEffect, useRef } from 'react';
import { useService } from '../util/services.js';
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultInputBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox, unthemedSelectBoxStyles } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
export const VoidInputBox = ({ onChangeText, initVal, placeholder, inputBoxRef, multiline }: {
onChangeText: (value: string) => void;
placeholder: string;
inputBoxRef: React.MutableRefObject<InputBox | null>;
multiline: boolean;
initVal: string;
}) => {
const contextViewProvider = useService('contextViewService');
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
// create and mount the HistoryInputBox
inputBoxRef.current = new InputBox(
containerRef.current,
contextViewProvider,
{
inputBoxStyles: {
...defaultInputBoxStyles,
inputBackground: 'transparent',
},
placeholder,
flexibleHeight: multiline,
flexibleMaxHeight: 500,
flexibleWidth: false,
}
);
inputBoxRef.current.value = initVal;
inputBoxRef.current.onDidChange((newStr) => {
console.log('CHANGE TEXT on inputbox', newStr)
onChangeText(newStr)
})
// cleanup
return () => {
if (inputBoxRef.current) {
inputBoxRef.current.dispose();
if (containerRef.current) {
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
}
inputBoxRef.current = null;
}
};
}, [inputBoxRef, contextViewProvider, placeholder, multiline, initVal, onChangeText]);
return <div ref={containerRef} className="w-full" />;
};
// export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, options }: {
// onChangeSelection: (value: string) => void;
// initVal: string;
// selectBoxRef: React.MutableRefObject<SelectBox | null>;
// options: readonly string[];
// }) => {
// const containerRef = useRef<HTMLDivElement>(null);
// const contextViewProvider = useService('contextViewService');
// useEffect(() => {
// if (!containerRef.current) return;
// const defaultIndex = options.indexOf(initVal);
// selectBoxRef.current = new SelectBox(
// options.map(opt => ({ text: opt })),
// defaultIndex,
// contextViewProvider,
// unthemedSelectBoxStyles
// );
// selectBoxRef.current.render(containerRef.current);
// selectBoxRef.current.onDidSelect(e => { onChangeSelection(e.selected); });
// // cleanup
// return () => {
// if (selectBoxRef.current) {
// selectBoxRef.current.dispose();
// if (containerRef.current) {
// while (containerRef.current.firstChild) {
// containerRef.current.removeChild(containerRef.current.firstChild);
// }
// }
// }
// };
// }, [options, initVal, onChangeSelection, contextViewProvider, selectBoxRef]);
// return <div ref={containerRef} className="w-full" />;
// };
@@ -1,4 +0,0 @@
import posthog from 'posthog-js';
export { posthog }
@@ -1,986 +0,0 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { posthog } from 'posthog-js'
import type { VoidConfig } from '../../../registerConfig.js';
import type { LLMMessage, OnText, OnError, OnFinalMessage, SendLLMMMessageParams, } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
voidConfig: VoidConfig;
_setAborter: (aborter: () => void) => void;
}) => 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
type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
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({ fullText: 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({ error: 'Invalid API key.' })
}
else {
onError({ error })
}
})
// 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 && error.status === 400) {
onError({ error: 'Invalid API key.' });
}
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 && error.status === 401) {
onError({ error: 'Invalid API key.' });
}
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({ newText: message, fullText })
}
else if (type === 'sources') {
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
fullText += filepath
onText({ newText: filepath, fullText })
}
// type: 'status' with an empty 'message' means last message
else if (type === 'status') {
if (!message) {
onFinalMessage({ fullText })
}
}
}
})
.catch(error => {
onError({ error })
});
}
export const sendLLMMessage = ({
messages,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
voidConfig,
logging: { loggingName }
}: SendLLMMMessageParams) => {
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) => {
posthog.capture(eventId, {
whichApi: voidConfig.default['whichApi'],
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
version: '2024-11-14',
...extras,
})
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText: OnText = ({ newText, fullText }) => {
if (_didAbort) return
onText_({ newText, fullText })
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText })
}
const onError: OnError = ({ error }) => {
console.error('sendLLMMessage onError:', error)
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 })
try {
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: `Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!` })
break;
}
}
catch (error) {
if (error instanceof Error) { onError({ error }) }
else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); }
; (_aborter as any)?.()
_didAbort = true
}
}
// // 6. Autocomplete
// const autocompleteProvider = new AutocompleteProvider(context);
// context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider('*', autocompleteProvider));
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
// // setupAutocomplete({ voidConfig, abortRef })
// // 7. Language Server
// console.log('run lsp')
// let disposable = vscode.commands.registerCommand('typeInspector.inspect', runTreeSitter);
// context.subscriptions.push(disposable);
// import { configFields, VoidConfig } from "../webviews/common/contextForConfig"
// import { FimInfo } from "./sendLLMMessage"
// type GetFIMPrompt = ({ voidConfig, fimInfo }: { voidConfig: VoidConfig, fimInfo: FimInfo, }) => string
// export const getFIMSystem: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
// switch (voidConfig.default.whichApi) {
// case 'ollama':
// return ''
// case 'anthropic':
// case 'openAI':
// case 'gemini':
// case 'greptile':
// case 'openRouter':
// case 'openAICompatible':
// case 'azure':
// default:
// return `You are given the START and END to a piece of code. Please FILL IN THE MIDDLE between the START and END.
// Instruction summary:
// 1. Return the MIDDLE of the code between the START and END.
// 2. Do not give an explanation, description, or any other code besides the middle.
// 3. Do not return duplicate code from either START or END.
// 4. Make sure the MIDDLE piece of code has balanced brackets that match the START and END.
// 5. The MIDDLE begins on the same line as START. Please include a newline character if you want to begin on the next line.
// 6. Around 90% of the time, you should return just one or a few lines of code. You should keep your outputs short unless you are confident the user is trying to write boilderplate code.
// # EXAMPLE
// ## START:
// \`\`\` python
// def add(a,b):
// return a + b
// def subtract(a,b):
// return a - b
// \`\`\`
// ## END:
// \`\`\` python
// def divide(a,b):
// return a / b
// \`\`\`
// ## EXPECTED OUTPUT:
// \`\`\` python
// def multiply(a,b):
// return a * b
// \`\`\`
// # EXAMPLE
// ## START:
// \`\`\` javascript
// const x = 1
// const y
// \`\`\`
// ## END:
// \`\`\` javascript
// const z = 3
// \`\`\`
// ## EXPECTED OUTPUT:
// \`\`\` javascript
// = 2
// \`\`\`
// `
// }
// }
// export const getFIMPrompt: GetFIMPrompt = ({ voidConfig, fimInfo }) => {
// const { prefix: fullPrefix, suffix: fullSuffix } = fimInfo
// const prefix = fullPrefix.split('\n').slice(-20).join('\n')
// const suffix = fullSuffix.split('\n').slice(0, 20).join('\n')
// console.log('prefix', JSON.stringify(prefix))
// console.log('suffix', JSON.stringify(suffix))
// if (!prefix.trim() && !suffix.trim()) return ''
// // TODO may want to trim the prefix and suffix
// switch (voidConfig.default.whichApi) {
// case 'ollama':
// if (voidConfig.ollama.model === 'codestral') {
// return `[SUFFIX]${suffix}[PREFIX] ${prefix}`
// } else if (voidConfig.ollama.model.includes('qwen')) {
// return `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`
// }
// return ''
// case 'anthropic':
// case 'openAI':
// case 'gemini':
// case 'greptile':
// case 'openRouter':
// case 'openAICompatible':
// case 'azure':
// default:
// return `## START:
// \`\`\`
// ${prefix}
// \`\`\`
// ## END:
// \`\`\`
// ${suffix}
// \`\`\`
// `
// }
// }
// Mathew - sendLLMMessage
// 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 { getFIMPrompt, getFIMSystem } from './getPrompt';
// export type AbortRef = { current: (() => void) }
// export type LLMMessageOnText = (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 LLMMessageOptions = { stopTokens?: string[] }
// type SendLLMMessageFnTypeInternal = (params: {
// mode: 'chat' | 'fim',
// messages: LLMMessage[],
// options?: LLMMessageOptions,
// onText: LLMMessageOnText,
// onFinalMessage: OnFinalMessage,
// onError: (error: string) => void,
// abortRef: AbortRef,
// voidConfig: VoidConfig,
// }) => void
// type SendLLMMessageFnTypeExternal = (params: (
// | { mode?: 'chat', messages: LLMMessage[], fimInfo?: undefined, }
// | { mode: 'fim', messages?: undefined, fimInfo: FimInfo, }
// ) & {
// options?: LLMMessageOptions,
// onText: LLMMessageOnText,
// onFinalMessage: OnFinalMessage,
// onError: (error: string) => void,
// abortRef: AbortRef,
// voidConfig: VoidConfig | null, // these may be absent
// }) => void
// export type FimInfo = {
// prefix: string,
// suffix: string,
// }
// const parseMaxTokensStr = (maxTokensStr: string) => {
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
// let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
// if (Number.isNaN(int))
// return undefined
// return int
// }
// // Anthropic
// const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
// 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
// });
// let did_abort = false
// // when receive text
// stream.on('text', (newText, fullText) => {
// if (did_abort) return
// onText(newText, fullText)
// })
// // when we get the final message on this stream (or when error/fail)
// stream.on('finalMessage', (claude_response) => {
// if (did_abort) return
// // stringify the response's content
// let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).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)
// }
// })
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// const abort = () => {
// did_abort = true
// stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
// }
// return { abort }
// };
// // Gemini
// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// let didAbort = false
// let fullText = ''
// abortRef.current = () => {
// didAbort = true
// }
// 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
// let 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 => {
// abortRef.current = () => {
// // response.stream.return(fullText)
// didAbort = true;
// }
// for await (const chunk of response.stream) {
// if (didAbort) return;
// 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, abortRef }) => {
// let didAbort = false
// let fullText = ''
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// abortRef.current = () => {
// didAbort = true;
// };
// let openai: OpenAI
// let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
// let 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 => {
// abortRef.current = () => {
// // response.controller.abort()
// didAbort = true;
// }
// // when receive text
// for await (const chunk of response) {
// if (didAbort) return;
// const newText = chunk.choices[0]?.delta?.content || '';
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// // when error/fail - 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 = ({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// let didAbort = false
// let fullText = ""
// const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
// abortRef.current = () => {
// didAbort = true;
// };
// type GenerateResponse = Awaited<ReturnType<(typeof ollama.generate)>>
// type ChatResponse = Awaited<ReturnType<(typeof ollama.chat)>>
// // First check if model exists
// ollama.list()
// .then(async models => {
// const installedModels = models.models.map(m => m.name.replace(/:latest$/, ''))
// const modelExists = installedModels.some(m => m.startsWith(voidConfig.ollama.model));
// if (!modelExists) {
// const errorMessage = `The model "${voidConfig.ollama.model}" is not available locally. Please run 'ollama pull ${voidConfig.ollama.model}' to download it first or
// try selecting one from the Installed models: ${installedModels.join(', ')}`;
// onText(errorMessage, errorMessage);
// onFinalMessage(errorMessage);
// return Promise.reject();
// }
// if (mode === 'fim') {
// // the fim prompt is the last message
// let prompt = messages[messages.length - 1].content
// return ollama.generate({
// model: voidConfig.ollama.model,
// prompt: prompt,
// stream: true,
// raw: true,
// options: { stop: options?.stopTokens }
// })
// }
// return ollama.chat({
// model: voidConfig.ollama.model,
// messages: messages,
// stream: true,
// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) }
// });
// })
// .then(async stream => {
// if (!stream) return;
// abortRef.current = () => {
// didAbort = true
// stream.abort()
// }
// for await (const chunk of stream) {
// if (didAbort) return;
// const newText = (mode === 'fim'
// ? (chunk as GenerateResponse).response
// : (chunk as ChatResponse).message.content
// )
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// .catch(error => {
// // Check if the error is a connection error
// if (error instanceof Error && error.message.includes('Failed to fetch')) {
// const errorMessage = 'Ollama service is not running. Please start the Ollama service and try again.';
// onText(errorMessage, errorMessage);
// onFinalMessage(errorMessage);
// } else if (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, abortRef }) => {
// let didAbort = false
// let fullText = ''
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// abortRef.current = () => {
// didAbort = true
// }
// 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
// .then(async responseArr => {
// if (didAbort)
// return
// for (let 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 } = 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 = ({ options, mode, messages, fimInfo, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// if (!voidConfig)
// return onError('No config file found for LLM.');
// // handle defaults
// if (!mode) mode = 'chat'
// if (!messages) messages = []
// // build messages
// if (mode === 'chat') {
// // nothing needed
// } else if (mode === 'fim') {
// fimInfo = fimInfo!
// const system = getFIMSystem({ voidConfig, fimInfo })
// const prompt = getFIMPrompt({ voidConfig, fimInfo })
// messages = ([
// { role: 'system', content: system },
// { role: 'user', content: prompt }
// ] as const)
// }
// // trim message content (Anthropic and other providers give an error if there is trailing whitespace)
// messages = messages.map(m => ({ ...m, content: m.content.trim() }))
// .filter(m => m.content !== '')
// if (messages.length === 0)
// return onError('No messages provided to LLM.');
// switch (voidConfig.default.whichApi) {
// case 'anthropic':
// return sendAnthropicMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'openAI':
// case 'openRouter':
// case 'openAICompatible':
// return sendOpenAIMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'gemini':
// return sendGeminiMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'ollama':
// return sendOllamaMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'greptile':
// return sendGreptileMsg({ options, mode, messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// default:
// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
// }
// }
@@ -3,8 +3,6 @@ import { defineConfig } from 'tsup'
export default defineConfig({
entry: [
'./src2/sidebar-tsx/Sidebar.tsx',
'./src2/util/sendLLMMessage.tsx',
'./src2/util/posthog.tsx',
'./src2/util/diffLines.tsx',
],
outDir: './out',
@@ -9,202 +9,7 @@ import { IEncryptionService } from '../../../../platform/encryption/common/encry
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
return {
description,
defaultVal,
enumArr,
}
}
const configString = (description: string, defaultVal: string) => {
return {
description,
defaultVal,
enumArr: undefined,
}
}
// fields you can customize (don't forget 'default' - it isn't included here!)
export const nonDefaultConfigFields = [
'anthropic',
'openAI',
'gemini',
'greptile',
'ollama',
'openRouter',
'openAICompatible',
'azure',
] as const
const voidConfigInfo: Record<
typeof nonDefaultConfigFields[number] | 'default', {
[prop: string]: {
description: string;
enumArr?: readonly string[] | undefined;
defaultVal: string;
};
}
> = {
default: {
whichApi: configEnum(
'API Provider.',
'anthropic',
nonDefaultConfigFields,
),
maxTokens: configEnum(
'Max number of tokens to output.',
'1024',
[
'default', // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
'1024',
'2048',
'4096',
'8192'
] as const,
),
},
anthropic: {
apikey: configString('Anthropic API key.', ''),
model: configEnum(
'Anthropic model to use.',
'claude-3-5-sonnet-20240620',
[
'claude-3-5-sonnet-20240620',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
] as const,
),
},
openAI: {
apikey: configString('OpenAI API key.', ''),
model: configEnum(
'OpenAI model to use.',
'gpt-4o',
[
'o1-preview',
'o1-mini',
'gpt-4o',
'gpt-4o-2024-05-13',
'gpt-4o-2024-08-06',
'gpt-4o-mini',
'gpt-4o-mini-2024-07-18',
'gpt-4-turbo',
'gpt-4-turbo-2024-04-09',
'gpt-4-turbo-preview',
'gpt-4-0125-preview',
'gpt-4-1106-preview',
'gpt-4',
'gpt-4-0613',
'gpt-3.5-turbo-0125',
'gpt-3.5-turbo',
'gpt-3.5-turbo-1106'
] as const
),
},
greptile: {
apikey: configString('Greptile API key.', ''),
githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
remote: configEnum(
'Repo location',
'github',
[
'github',
'gitlab'
] as const
),
repository: configString('Repository identifier in "owner / repository" format.', ''),
branch: configString('Name of the branch to use.', 'main'),
},
ollama: {
endpoint: configString(
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode - webview://*" ollama serve`.',
'http://127.0.0.1:11434'
),
model: configEnum(
'Ollama model to use.',
'codestral',
['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b'] as const
),
},
openRouter: {
model: configString(
'OpenRouter model to use.',
'openai/gpt-4o'
),
apikey: configString('OpenRouter API key.', ''),
},
openAICompatible: {
endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'),
model: configString('The name of the model to use.', 'gpt-4o'),
apikey: configString('Your API key.', ''),
},
azure: {
// 'void.azure.apiKey': {
// 'type': 'string',
// 'description': 'Azure API key.'
// },
// 'void.azure.deploymentId': {
// 'type': 'string',
// 'description': 'Azure API deployment ID.'
// },
// 'void.azure.resourceName': {
// 'type': 'string',
// 'description': 'Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`'
// },
// 'void.azure.providerSettings': {
// 'type': 'object',
// 'properties': {
// 'baseURL': {
// 'type': 'string',
// 'default': 'https://${resourceName}.openai.azure.com/openai/deployments',
// 'description': 'Azure API base URL.'
// },
// 'headers': {
// 'type': 'object',
// 'description': 'Custom headers to include in the requests.'
// }
// }
// },
},
gemini: {
apikey: configString('Google API key.', ''),
model: configEnum(
'Gemini model to use.',
'gemini-1.5-flash',
[
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
] as const
),
},
}
// this is the type that comes with metadata like desc, default val, etc
export type VoidConfigInfo = typeof voidConfigInfo
export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number]
// this is the type that specifies the user's actual config
export type PartialVoidConfig = {
[K in keyof typeof voidConfigInfo]?: {
[P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal']
}
}
export type VoidConfig = {
[K in keyof typeof voidConfigInfo]: {
[P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal']
}
}
import { PartialVoidConfig, VoidConfig, nonDefaultConfigFields, voidConfigInfo, VoidConfigField, VoidConfigInfo } from '../../../../platform/void/common/configTypes.js';
const getVoidConfig = (partialVoidConfig: PartialVoidConfig): VoidConfig => {
@@ -30,8 +30,6 @@ import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { LLMMessageServiceParams } from '../../../../platform/void/common/llmMessageTypes.js';
import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js';
// import { ISendLLMMessageService } from '../../../../platform/void/common/sendLLMMessage.js';
// import { sendLLMMessage } from './react/out/util/sendLLMMessage.js';
// gets converted to --vscode-void-greenBG, see void.css
@@ -1,52 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
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 { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { posthog } from './react/out/util/posthog.js'
// const buildEnv = 'development';
// const buildNumber = '1.0.0';
// const isMac = process.platform === 'darwin';
// // TODO use commandKey
// const commandKey = isMac ? '⌘' : 'Ctrl';
// const systemInfo = {
// buildEnv,
// buildNumber,
// isMac,
// }
interface IMetricsService {
readonly _serviceBrand: undefined;
}
const IMetricsService = createDecorator<IMetricsService>('metricsService');
class MetricsService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
constructor(
@ITelemetryService private readonly _telemetryService: ITelemetryService
) {
super()
posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
})
const deviceId = this._telemetryService.devDeviceId
console.debug('deviceId', deviceId)
posthog.identify(deviceId)
}
}
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
@@ -11,7 +11,6 @@ import {
} from '../../../common/views.js';
import * as nls from '../../../../nls.js';
import * as dom from '../../../../base/browser/dom.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { localize } from '../../../../nls.js';
@@ -33,7 +32,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.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';
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
@@ -48,6 +47,7 @@ import { IFileService } from '../../../../platform/files/common/files.js';
import { IInlineDiffsService } from './registerInlineDiffs.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
@@ -69,6 +69,10 @@ export type ReactServicesType = {
modelService: IModelService;
inlineDiffService: IInlineDiffsService;
sendLLMMessageService: ISendLLMMessageService;
clipboardService: IClipboardService;
contextViewService: IContextViewService;
contextMenuService: IContextMenuService;
}
// ---------- Define viewpane ----------
@@ -100,9 +104,8 @@ class VoidSidebarViewPane extends ViewPane {
protected override renderBody(parent: HTMLElement): void {
super.renderBody(parent);
const { root } = dom.h('div@root')
dom.append(parent, root);
parent.style.overflow = 'auto'
parent.style.userSelect = 'text'
// gets set immediately
this.instantiationService.invokeFunction(accessor => {
@@ -114,8 +117,11 @@ class VoidSidebarViewPane extends ViewPane {
modelService: accessor.get(IModelService),
inlineDiffService: accessor.get(IInlineDiffsService),
sendLLMMessageService: accessor.get(ISendLLMMessageService),
clipboardService: accessor.get(IClipboardService),
contextViewService: accessor.get(IContextViewService),
contextMenuService: accessor.get(IContextMenuService),
}
mountFn(root, services);
mountFn(parent, services);
});
}
@@ -23,17 +23,19 @@ export type CodeStagingSelection = {
fileURI: URI;
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'user';
content: string; // content sent to the llm
displayContent: string; // content displayed to user
content: string | null; // content sent to the llm - yes, allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - yes, allowed to be '', will be ignored
selections: CodeSelection[] | null; // the user's selection
}
| {
role: 'assistant';
content: string; // content received from LLM
displayContent: string | undefined; // content displayed to user (this is the same as content for now)
content: string | null; // content received from LLM - yes, allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - yes, allowed to be '', will be ignored
}
| {
role: 'system';
@@ -133,6 +135,8 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService {
}
switchToThread(threadId: string) {
console.log('threadId', threadId)
console.log('messages', this.state.allThreads[threadId].messages)
this._setState({ _currentThreadId: threadId }, true)
}
@@ -194,3 +198,4 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService {
}
registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);
@@ -7,14 +7,11 @@
import './registerActions.js'
// register Settings
import './registerConfig.js'
import './registerConfig.js' // TODO move this to platform
// register inline diffs
import './registerInlineDiffs.js'
// register Posthog metrics
import './registerMetrics.js'
// register Sidebar chat
import './registerSidebar.js'