diff --git a/.idx/dev.nix b/.idx/dev.nix new file mode 100644 index 00000000..83eb293f --- /dev/null +++ b/.idx/dev.nix @@ -0,0 +1,48 @@ +# Created for Void +# To learn more about how to use Nix to configure your environment +# see: https://developers.google.com/idx/guides/customize-idx-env +{pkgs}: { + # Which nixpkgs channel to use. + channel = "stable-23.11"; # or "unstable" + # Use https://search.nixos.org/packages to find packages + packages = [ + pkgs.nodejs_20 + pkgs.yarn + pkgs.nodePackages.pnpm + pkgs.bun + pkgs.gh + ]; + # Sets environment variables in the workspace + env = {}; + idx = { + # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" + extensions = [ + # "vscodevim.vim" + ]; + workspace = { + # Runs when a workspace is first created with this `dev.nix` file + onCreate = { + npm-install = "npm ci --no-audit --prefer-offline --no-progress --timing"; + # Open editors for the following files by default, if they exist: + default.openFiles = [ + # Cover all the variations of language, src-dir, router (app/pages) + "pages/index.tsx" "pages/index.jsx" + "src/pages/index.tsx" "src/pages/index.jsx" + "app/page.tsx" "app/page.jsx" + "src/app/page.tsx" "src/app/page.jsx" + ]; + }; + # To run something each time the workspace is (re)started, use the `onStart` hook + }; + # Enable previews and customize configuration + previews = { + enable = true; + previews = { + web = { + command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"]; + manager = "web"; + }; + }; + }; + }; +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0125ac46..0bd47caf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,39 +9,10 @@ There are a few ways to contribute: - Submit Issues/Docs/Bugs ([Issues](https://github.com/voideditor/void/issues)) -## 1. Building the Extension +## Building the full IDE -Here's how you can start contributing to the Void extension. This is where you should get started if you're new. +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. -1. Clone the repository: - -``` -git clone https://github.com/voideditor/void -``` - -2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, _don't_ just cd into it). - -3. Install dependencies: - -``` -npm install -``` - -1. Compile the React files by running `npm run build`. This build command converts all the Tailwind/React entrypoint files into raw .css and .js files in `dist/`. - -``` -npm run build -``` - -5. Run the extension in a new window by pressing F5. - -This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press Ctrl+Shift+P, select "Debug: Start Debugging", and select "VSCode Extension Development". - -## 2. Building the full IDE - -If you want to work on the full IDE, please follow the steps below. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. Also feel free to submit an issue or get in touch with us with any build errors. - - ### a. Build Prerequisites - Mac @@ -51,8 +22,6 @@ If you're using a Mac, make sure you have Python and XCode installed (you probab If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them. -Open the installer for Visual Studio 2022 (or VS Build Tools). This is often automatic. - Go to the "Workloads" tab and select: - `Desktop development with C++` - `Node.js build tools` @@ -66,13 +35,14 @@ Finally, click Install. ### c. Build Prerequisites - Linux -We haven't created prerequisite steps for building on Linux yet, but you can follow [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). +First, make sure you've installed NodeJS and run `npm install -g node-gyp`. Then: +- Debian (Ubuntu, etc) - `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`. +- Red Hat (Fedora, etc) - `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`. +- Others - see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). ### Build instructions -Before building Void, please follow the prerequisite steps above for your operating system. Also, make sure you've already built and compiled the Void extension (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`). - -To build Void, first open `void/` in VSCode. Then: +To build Void, first follow the prerequisite steps above for your operating system and open `void/` inside VSCode. Then: 1. Install all dependencies. @@ -80,7 +50,9 @@ To build Void, first open `void/` in VSCode. Then: npm install ``` -2. Press Ctrl+Shift+B, or if you prefer using the terminal run `npm run watch`. +2. Run `cd ./src/vs/workbench/contrib/void/browser/react/` and then `node ./build.js` to build Void's external dependencies (our React components, etc). + +3. Press Ctrl+Shift+B, or if you prefer using the terminal run `npm run watch`. This can take ~5 min. @@ -97,11 +69,10 @@ If you ran `npm run watch`, the build is done when you see something like this: -1. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `/.scripts/code.bat` (Windows). This should open up the built IDE! +4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE! You can always press Ctrl+Shift+P and run "Reload Window" inside the new window to see changes without re-building. -Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page! - +Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page. ### Common Fixes @@ -147,7 +118,7 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo ## Submitting a Pull Request -Please submit a pull request once you've made a change. You don't need to submit an issue. +Please submit a pull request once you've made a change. You don't need to submit an issue. Please don't use AI to write your PR 🙂. diff --git a/VOID_USEFUL_LINKS.md b/VOID_USEFUL_LINKS.md index f2c58d55..ef7cbc16 100644 --- a/VOID_USEFUL_LINKS.md +++ b/VOID_USEFUL_LINKS.md @@ -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). diff --git a/build/npm/dirs.js b/build/npm/dirs.js index bd332502..7b11bde9 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -7,8 +7,6 @@ const fs = require('fs'); // Complete list of directories where npm should be executed to install node modules const dirs = [ - 'extensions/void', // <-- Void - '', 'build', 'extensions', @@ -55,6 +53,11 @@ const dirs = [ 'test/smoke', '.vscode/extensions/vscode-selfhost-import-aid', '.vscode/extensions/vscode-selfhost-test-provider', + + // Void added these: + // 'extensions/void', + // 'void-imports', + ]; if (fs.existsSync(`${__dirname}/../../.build/distro/npm`)) { diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index e38c2cb0..10d5d95b 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -139,3 +139,21 @@ for (let dir of dirs) { cp.execSync('git config pull.rebase merges'); cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs'); + + +// // Void added this (inject void-imports into project): +// const buildVoidImports = () => { +// console.log('\n\nVoid is injecting void-imports...') +// cp.execSync(`npm install`, { // this goes here, not in postinstall, because we need to +// env: process.env, +// cwd: path.join(__dirname, '..', '..', '/void-imports'), +// stdio: 'inherit' +// }); +// cp.execSync(`node build-index.mjs`, { +// env: process.env, +// cwd: path.join(__dirname, '..', '..', '/void-imports'), +// stdio: 'inherit' +// }); +// console.log('Done injecting void-imports.') +// } +// buildVoidImports() diff --git a/extensions/void/.eslintrc b/extensions/void/.eslintrc deleted file mode 100644 index f47a9afd..00000000 --- a/extensions/void/.eslintrc +++ /dev/null @@ -1,56 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": [ - "@typescript-eslint", - "react", - "react-hooks" - ], - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended" - ], - "rules": { - "@typescript-eslint/naming-convention": [ - "warn", - { - "selector": "import", - "format": [ - "camelCase", - "PascalCase" - ] - } - ], - "curly": "off", - "eqeqeq": "warn", - "no-empty": "off", - "no-throw-literal": "warn", - "semi": "off", - "no-unused-vars": "off", - "react-hooks/exhaustive-deps": "warn" - }, - "ignorePatterns": [ - "out", - "dist", - "**/*.d.ts" - ], - "settings": { - "react": { - "version": "detect" - } - }, - "env": { - "browser": true, // enable browser globals linting (window, document, console, etc) - "es6": true, // enable ES6 linting - "node": true, // enable Node linting (things like Buffer which is used in file reading, etc) - "mocha": true // enable Mocha linting - } -} \ No newline at end of file diff --git a/extensions/void/.gitignore b/extensions/void/.gitignore deleted file mode 100644 index 0b60dfa1..00000000 --- a/extensions/void/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -out -dist -node_modules -.vscode-test/ -*.vsix diff --git a/extensions/void/.vscode-test.mjs b/extensions/void/.vscode-test.mjs deleted file mode 100644 index b62ba25f..00000000 --- a/extensions/void/.vscode-test.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfig } from '@vscode/test-cli'; - -export default defineConfig({ - files: 'out/test/**/*.test.js', -}); diff --git a/extensions/void/.vscode/extensions.json b/extensions/void/.vscode/extensions.json deleted file mode 100644 index 186459d5..00000000 --- a/extensions/void/.vscode/extensions.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "dbaeumer.vscode-eslint", - "ms-vscode.extension-test-runner" - ] -} diff --git a/extensions/void/.vscode/launch.json b/extensions/void/.vscode/launch.json deleted file mode 100644 index 17ab1d57..00000000 --- a/extensions/void/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -// A launch configuration that compiles the extension and then opens it inside a new window -// Use IntelliSense to learn about possible attributes. -// Hover to view descriptions of existing attributes. -// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--enable-proposed-api=void.void", - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - } - ] -} \ No newline at end of file diff --git a/extensions/void/.vscode/settings.json b/extensions/void/.vscode/settings.json deleted file mode 100644 index 3100dfe2..00000000 --- a/extensions/void/.vscode/settings.json +++ /dev/null @@ -1,18 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "out": false, - "**/node_modules": true - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", -} diff --git a/extensions/void/.vscode/tasks.json b/extensions/void/.vscode/tasks.json deleted file mode 100644 index 3b17e53b..00000000 --- a/extensions/void/.vscode/tasks.json +++ /dev/null @@ -1,20 +0,0 @@ -// See https://go.microsoft.com/fwlink/?LinkId=733558 -// for the documentation about the tasks.json format -{ - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} diff --git a/extensions/void/.vscodeignore b/extensions/void/.vscodeignore deleted file mode 100644 index 72aa0fe2..00000000 --- a/extensions/void/.vscodeignore +++ /dev/null @@ -1,11 +0,0 @@ -.vscode/** -.vscode-test/** -src/** -.gitignore -.yarnrc -vsc-extension-quickstart.md -**/tsconfig.json -**/.eslintrc.json -**/*.map -**/*.ts -**/.vscode-test.* diff --git a/extensions/void/README.md b/extensions/void/README.md deleted file mode 100644 index d455ba30..00000000 --- a/extensions/void/README.md +++ /dev/null @@ -1,11 +0,0 @@ -Please see the `CONTRIBUTING.md` for information on how to contribute :)! - - -Here's an overview on how the extension works: - -- The extension mounts in `extension.ts`. - -- The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a ` - - `; - - webview.html = webviewHTML - - webview.options = { - enableScripts: true, - localResourceRoots: [extensionUri] - }; -} diff --git a/extensions/void/src/extension/findDiffs.ts b/extensions/void/src/extension/findDiffs.ts deleted file mode 100644 index 02d073a7..00000000 --- a/extensions/void/src/extension/findDiffs.ts +++ /dev/null @@ -1,132 +0,0 @@ - -import { Range } from 'vscode'; -import { diffLines, Change } from 'diff'; -import { BaseDiff } from '../common/shared_types'; - - - -// class Range { -// range: any; -// constructor(startLine, startCol, endLine, endCol) { -// const range = { -// startLine, -// startCol, -// endLine, -// endCol, -// }; -// this.range = range; -// } -// } - - - -// Andrew diff algo: -export type SuggestedEdit = { - // start/end of current file - newRange: Range; - - // start/end of original file - originalRange: Range; - type: 'insertion' | 'deletion' | 'edit', - originalContent: string, // original content (originalfile[originalStart...originalEnd]) - newContent: string, -} - -export function findDiffs(oldStr: string, newStr: string) { - // an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it) - const lineByLineChanges: Change[] = diffLines(oldStr, newStr); - lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed) - - let oldFileLineNum: number = 0; - let newFileLineNum: number = 0; - - let streakStartInNewFile: number | undefined = undefined - let streakStartInOldFile: number | undefined = undefined - - let oldStrLines = oldStr.split('\n') - let newStrLines = newStr.split('\n') - - const replacements: BaseDiff[] = [] - for (let line of lineByLineChanges) { - - // no change on this line - if (!line.added && !line.removed) { - - // do nothing - - // if we were on a streak of +s and -s, end it - if (streakStartInNewFile !== undefined) { - let type: 'edit' | 'insertion' | 'deletion' = 'edit' - - let startLine = streakStartInNewFile - let endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it - let startCol = 0 - let endCol = Number.MAX_SAFE_INTEGER - - let originalStartLine = streakStartInOldFile! - let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it - let originalStartCol = 0 - let originalEndCol = Number.MAX_SAFE_INTEGER - - let newContent = newStrLines.slice(startLine, endLine + 1).join('\n') - let originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n') - - // if the range is empty, mark it as a deletion / insertion (both won't be true at once) - // DELETION - if (endLine === startLine - 1) { - type = 'deletion' - endLine = startLine - startCol = 0 - endCol = 0 - newContent += '\n' - } - - // INSERTION - else if (originalEndLine === originalStartLine - 1) { - type = 'insertion' - originalEndLine = originalStartLine - originalStartCol = 0 - originalEndCol = 0 - } - - const replacement: BaseDiff = { - type, - range: new Range(startLine, startCol, endLine, endCol), - code: newContent, - originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol), - originalCode: originalContent, - } - - replacements.push(replacement) - - streakStartInNewFile = undefined - streakStartInOldFile = undefined - } - oldFileLineNum += line.count ?? 0; - newFileLineNum += line.count ?? 0; - } - - // line was removed from old file - else if (line.removed) { - // if we weren't on a streak, start one on this current line num - if (streakStartInNewFile === undefined) { - streakStartInNewFile = newFileLineNum - streakStartInOldFile = oldFileLineNum - } - oldFileLineNum += line.count ?? 0 // we processed the line so add 1 - } - - // line was added to new file - else if (line.added) { - // if we weren't on a streak, start one on this current line num - if (streakStartInNewFile === undefined) { - streakStartInNewFile = newFileLineNum - streakStartInOldFile = oldFileLineNum - } - newFileLineNum += line.count ?? 0; // we processed the line so add 1 - } - } // end for - - console.debug('Replacements', replacements) - return replacements -} diff --git a/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts b/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts deleted file mode 100644 index 8ef55198..00000000 --- a/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts +++ /dev/null @@ -1,57 +0,0 @@ -// renders the code from `src/sidebar` - -import * as vscode from 'vscode'; -import { updateWebviewHTML as _updateWebviewHTML, updateWebviewHTML } from '../extensionLib/updateWebviewHTML'; - -// this comes from vscode.proposed.editorInsets.d.ts -declare module 'vscode' { - export interface WebviewEditorInset { - readonly editor: vscode.TextEditor; - readonly line: number; - readonly height: number; - readonly webview: vscode.Webview; - readonly onDidDispose: Event; - dispose(): void; - } - export namespace window { - export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset; - } -} - - - -export class CtrlKWebviewProvider { - - private readonly _extensionUri: vscode.Uri - - private _idPool = 0 - - - - constructor(context: vscode.ExtensionContext) { - this._extensionUri = context.extensionUri - } - - onPressCtrlK() { - - // // TODO if currently selecting a ctrl k element, just focus it and do nothing - - - // const inset = vscode.window.createWebviewTextEditorInset(editor, line, height); - - - // const newCtrlKId = this._idPool++ - // updateWebviewHTML(inset.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/ctrlk/index.js', cssOutLocation: 'dist/webviews/styles.css' }, - // { id: newCtrlKId } - // ) - - // ctrlKWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar)); - - - } - - onDisposeCtrlK() { - - } - -} diff --git a/extensions/void/src/extension/providers/SidebarWebviewProvider.ts b/extensions/void/src/extension/providers/SidebarWebviewProvider.ts deleted file mode 100644 index 0ca1d895..00000000 --- a/extensions/void/src/extension/providers/SidebarWebviewProvider.ts +++ /dev/null @@ -1,30 +0,0 @@ -// renders the code from `src/sidebar` - -import * as vscode from 'vscode'; -import { updateWebviewHTML as _updateWebviewHTML } from '../extensionLib/updateWebviewHTML'; - -export class SidebarWebviewProvider implements vscode.WebviewViewProvider { - public static readonly viewId = 'void.viewnumberone'; - - public webview: Promise // used to send messages to the webview, resolved by _res in resolveWebviewView - private _res: (c: vscode.Webview) => void // used to resolve the webview - - private readonly _extensionUri: vscode.Uri - - constructor(context: vscode.ExtensionContext) { - // const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code - this._extensionUri = context.extensionUri - - let temp_res: typeof this._res | undefined = undefined - this.webview = new Promise((res, rej) => { temp_res = res }) - if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined") - this._res = temp_res - } - - // called internally by vscode - resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken,) { - const webview = webviewView.webview; - _updateWebviewHTML(webview, this._extensionUri, { jsOutLocation: 'dist/webviews/sidebar/index.js', cssOutLocation: 'dist/webviews/styles.css' }) - this._res(webview); // resolve webview and _webviewView - } -} diff --git a/extensions/void/src/test/extension.test.ts b/extensions/void/src/test/extension.test.ts deleted file mode 100644 index 4ca0ab41..00000000 --- a/extensions/void/src/test/extension.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; - -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); - - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); -}); diff --git a/extensions/void/src/webviews/common/contextForConfig.tsx b/extensions/void/src/webviews/common/contextForConfig.tsx deleted file mode 100644 index e1333154..00000000 --- a/extensions/void/src/webviews/common/contextForConfig.tsx +++ /dev/null @@ -1,289 +0,0 @@ -import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" -import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi" - -const configEnum = (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 configFields = [ - 'anthropic', - 'openAI', - 'gemini', - 'greptile', - 'ollama', - 'openRouter', - 'openAICompatible', - 'azure', -] as const - - - -const voidConfigInfo: Record< - typeof configFields[number] | 'default', { - [prop: string]: { - description: string, - enumArr?: readonly string[] | undefined, - defaultVal: string, - }, - } -> = { - default: { - whichApi: configEnum( - "API Provider.", - 'anthropic', - configFields, - ), - - 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' - ), - // TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own - model: configEnum( - 'Ollama model to use.', - 'llama3.1', - ["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.2", "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 -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'] - } -} - - - -export const getVoidConfigFromPartial = (partialVoidConfig: PartialVoidConfig): VoidConfig => { - const config = {} as PartialVoidConfig - for (let field of [...configFields, 'default'] as const) { - config[field] = {} - for (let prop in voidConfigInfo[field]) { - config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal - } - } - return config as VoidConfig -} - -const defaultVoidConfig: VoidConfig = getVoidConfigFromPartial({}) - -// const [stateRef, setState] = useInstantState(initVal) -// setState instantly changes the value of stateRef instead of having to wait until the next render -const useInstantState = (initVal: T) => { - const stateRef = useRef(initVal) - const [_, setS] = useState(initVal) - const setState = useCallback((newVal: T) => { - setS(newVal); - stateRef.current = newVal; - }, []) - return [stateRef as React.RefObject, setState] as const // make s.current readonly - setState handles all changes -} - - - -type SetConfigParamType = (field: K, param: keyof VoidConfigInfo[K], newVal: string) => void - -type ConfigValueType = { - voidConfig: VoidConfig, - voidConfigInfo: VoidConfigInfo, - partialVoidConfig: PartialVoidConfig, - setConfigParam: SetConfigParamType -} - - -const ConfigContext = createContext(undefined as unknown as ConfigValueType) - -export function ConfigProvider({ children }: { children: ReactNode }) { - const [partialVoidConfig, setPartialVoidConfig] = useInstantState({}) // the user's selections - const [voidConfig, setVoidConfig] = useState(defaultVoidConfig) - - - // get the config on mount - useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' }) - awaitVSCodeResponse('partialVoidConfig').then((m) => { - setPartialVoidConfig(m.partialVoidConfig) - const newFullConfig = getVoidConfigFromPartial(m.partialVoidConfig) - setVoidConfig(newFullConfig) - }) - }, [setPartialVoidConfig]) - - // return the provider - return ( { - const newPartialConfig: PartialVoidConfig = { - ...partialVoidConfig.current, - [field]: { - ...partialVoidConfig.current?.[field], - [param]: newVal - } - } - setPartialVoidConfig(newPartialConfig) - const newFullConfig = getVoidConfigFromPartial(newPartialConfig) - setVoidConfig(newFullConfig) - getVSCodeAPI().postMessage({ type: 'persistPartialVoidConfig', partialVoidConfig: newPartialConfig }) - } - }} - > - {children} - - ) -} - -export function useVoidConfig(): ConfigValueType { - const context = useContext(ConfigContext) - if (context === undefined) { - throw new Error("useVoidConfig missing Provider") - } - return context -} diff --git a/extensions/void/src/webviews/common/contextForProps.tsx b/extensions/void/src/webviews/common/contextForProps.tsx deleted file mode 100644 index 6775fcb1..00000000 --- a/extensions/void/src/webviews/common/contextForProps.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" - -const PropsContext = createContext(undefined as unknown as any) - -// provider for whatever came in data-void-props -export function PropsProvider({ children, rootElement }: { children: ReactNode, rootElement: HTMLElement }) { - - const [props, setProps] = useState(null) - - // update props when rootElement changes - useEffect(() => { - let props = rootElement.getAttribute("data-void-props") - let propsObj: object | null = null - if (props !== null) { - propsObj = JSON.parse(decodeURIComponent(props)) - } - setProps(propsObj) - }, [rootElement]) - - return ( - - {children} - - ) -} - -export function useVoidProps(): T | null { - // context is the "value" from above - const context: T | null | undefined = useContext(PropsContext) - // only undefined if has no provider - if (context === undefined) { - throw new Error("useVoidProps missing Provider") - } - return context -} - diff --git a/extensions/void/src/webviews/common/contextForThreads.tsx b/extensions/void/src/webviews/common/contextForThreads.tsx deleted file mode 100644 index d2ab97be..00000000 --- a/extensions/void/src/webviews/common/contextForThreads.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" -import { ChatMessage, ChatThreads } from "../../common/shared_types" -import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi" - - -// a "thread" means a chat message history -type ConfigForThreadsValueType = { - readonly getAllThreads: () => ChatThreads; - readonly getCurrentThread: () => ChatThreads[string] | null; - addMessageToHistory: (message: ChatMessage) => void; - switchToThread: (threadId: string) => void; - startNewThread: () => void; -} - -const ThreadsContext = createContext(undefined as unknown as ConfigForThreadsValueType) - -const createNewThread = () => { - const now = new Date().toISOString() - return { - id: new Date().getTime().toString(), - createdAt: now, - lastModified: now, - messages: [], - } -} - - -// const [stateRef, setState] = useInstantState(initVal) -// setState instantly changes the value of stateRef instead of having to wait until the next render -const useInstantState = (initVal: T) => { - const stateRef = useRef(initVal) - const [_, setS] = useState(initVal) - const setState = useCallback((newVal: T) => { - setS(newVal); - stateRef.current = newVal; - }, []) - return [stateRef as React.RefObject, setState] as const // make s.current readonly - setState handles all changes -} - - -export function ThreadsProvider({ children }: { children: ReactNode }) { - const [allThreadsRef, setAllThreads] = useInstantState({}) - const [currentThreadIdRef, setCurrentThreadId] = useInstantState(null) - - // this loads allThreads in on mount - useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getAllThreads' }) - awaitVSCodeResponse('allThreads') - .then(response => { - setAllThreads(response.threads) - }) - }, [setAllThreads]) - - - return ( - allThreadsRef.current ?? {}, - getCurrentThread: () => currentThreadIdRef.current ? allThreadsRef.current?.[currentThreadIdRef.current] ?? null : null, - addMessageToHistory: (message: ChatMessage) => { - let currentThread: ChatThreads[string] - if (!(currentThreadIdRef.current === null || allThreadsRef.current === null)) { - currentThread = allThreadsRef.current[currentThreadIdRef.current] - } - else { - currentThread = createNewThread() - setCurrentThreadId(currentThread.id) - } - - setAllThreads({ - ...allThreadsRef.current, - [currentThread.id]: { - ...currentThread, - lastModified: new Date().toISOString(), - messages: [...currentThread.messages, message], - } - }) - - getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread }) - }, - switchToThread: (threadId: string) => { - setCurrentThreadId(threadId); - }, - startNewThread: () => { - const newThread = createNewThread() - setAllThreads({ - ...allThreadsRef.current, - [newThread.id]: newThread - }) - setCurrentThreadId(newThread.id) - }, - }} - > - {children} - - ) -} - -export function useThreads(): ConfigForThreadsValueType { - const context = useContext(ThreadsContext) - if (context === undefined) { - throw new Error("useThreads missing Provider") - } - return context -} - diff --git a/extensions/void/src/webviews/common/getVscodeApi.ts b/extensions/void/src/webviews/common/getVscodeApi.ts deleted file mode 100644 index cf9d53ea..00000000 --- a/extensions/void/src/webviews/common/getVscodeApi.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useEffect } from "react"; -import { MessageFromSidebar, MessageToSidebar, } from "../../common/shared_types"; -import { v4 as uuidv4 } from 'uuid'; - - -type Command = MessageToSidebar['type'] - -// messageType -> res[] -const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = { - "ctrl+l": [], - "ctrl+k": [], - "files": [], - "partialVoidConfig": [], - "startNewThread": [], - "allThreads": [], - "toggleThreadSelector": [], - "toggleSettings": [], - "deviceId": [], -} - -// messageType -> id -> res -const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = { - "ctrl+l": {}, - "ctrl+k": {}, - "files": {}, - "partialVoidConfig": {}, - "startNewThread": {}, - "allThreads": {}, - "toggleThreadSelector": {}, - "toggleSettings": {}, - "deviceId": {} -} - - -// use this function to await responses -export const awaitVSCodeResponse = (c: C) => { - let result: Promise = new Promise((res, rej) => { - onetimeCallbacks[c].push(res) - }) - return result -} - - -// use this function to add a listener to a certain type of message -export const useOnVSCodeMessage = (messageType: C, fn: (e: MessageToSidebar & { type: C }) => void) => { - useEffect(() => { - const mType = messageType - const callbackId: string = uuidv4(); - // @ts-ignore - callbacks[mType][callbackId] = fn; - return () => { delete callbacks[mType][callbackId] } - }, [messageType, fn]) -} - - - -// this function gets called whenever sidebar receives a message - it should only mount once -export const onMessageFromVSCode = (m: MessageToSidebar) => { - // resolve all promises for this message type - for (let res of onetimeCallbacks[m.type]) { - res(m) - onetimeCallbacks[m.type].splice(0) // clear the array - } - // call the listener for this message type - for (let res of Object.values(callbacks[m.type])) { - res(m) - } -} - - - -type AcquireVsCodeApiType = () => { - postMessage(message: MessageFromSidebar): void; - // setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true } - // getState(): any; -}; - -// VS Code exposes the function acquireVsCodeApi() to us, this variable makes sure it only gets called once -let vsCodeApi: ReturnType | undefined; - -export function getVSCodeAPI(): ReturnType { - if (vsCodeApi) - return vsCodeApi; - - try { - // @ts-expect-error - // eslint-disable-next-line no-undef - vsCodeApi = acquireVsCodeApi(); - return vsCodeApi!; - } catch (error) { - console.error('Failed to acquire VS Code API:', error); - throw new Error('This script must be run in a VS Code webview context'); - } -} diff --git a/extensions/void/src/webviews/common/mount.tsx b/extensions/void/src/webviews/common/mount.tsx deleted file mode 100644 index a07b46b7..00000000 --- a/extensions/void/src/webviews/common/mount.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useEffect } from "react"; -import * as ReactDOM from "react-dom/client" -import { MessageToSidebar } from "../../common/shared_types"; -import { getVSCodeAPI, awaitVSCodeResponse, onMessageFromVSCode } from "./getVscodeApi"; -import { initPosthog, identifyUser } from "./posthog"; -import { ThreadsProvider } from "./contextForThreads"; -import { ConfigProvider } from "./contextForConfig"; -import { PropsProvider } from "./contextForProps"; - -const ListenersAndTracking = () => { - // initialize posthog - useEffect(() => { - initPosthog() - }, []) - - // when we get the deviceid, identify the user - useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getDeviceId' }); - awaitVSCodeResponse('deviceId').then((m => { - identifyUser(m.deviceId) - })) - }, []) - - // Receive messages from the VSCode extension - useEffect(() => { - const listener = (event: MessageEvent) => { - const m = event.data as MessageToSidebar; - onMessageFromVSCode(m) - } - window.addEventListener('message', listener); - return () => window.removeEventListener('message', listener) - }, []) - - return null -} - - - - -export const mount = (children: React.ReactNode) => { - - if (typeof document === "undefined") { - console.error("index.tsx error: document was undefined") - return - } - - // mount the sidebar on the id="root" element - const rootElement = document.getElementById("root")! - // console.log("Void root Element:", rootElement) - - const content = (<> - - - - - - {children} - - - - ) - - const root = ReactDOM.createRoot(rootElement) - root.render(content); - -} diff --git a/extensions/void/src/webviews/common/posthog.tsx b/extensions/void/src/webviews/common/posthog.tsx deleted file mode 100644 index 10690728..00000000 --- a/extensions/void/src/webviews/common/posthog.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import posthog from 'posthog-js' - - -export const identifyUser = (id: string) => { - posthog.identify(id) -} - -export const captureEvent = (eventId: string, properties: object) => { - posthog.capture(eventId, properties) -} - -export const initPosthog = () => { - // We send absolutely no code to the server. We only track usage metrics like button clicks, etc. This might change and we might eventually add an opt-in or opt-out. - 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 - } - ) -} \ No newline at end of file diff --git a/extensions/void/src/webviews/ctrlk/CtrlK.tsx b/extensions/void/src/webviews/ctrlk/CtrlK.tsx deleted file mode 100644 index ac167005..00000000 --- a/extensions/void/src/webviews/ctrlk/CtrlK.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useState } from 'react'; -import { useOnVSCodeMessage } from '../common/getVscodeApi'; - - -export const CtrlK = () => { - - const [x, sx] = useState('abc') - - useOnVSCodeMessage('ctrl+k', () => { - console.log('Ctrl+K pressed') - sx('Pressed ctrl+k') - }) - - // const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {}) - // inset.webview.html = ` - // - // Hello World! - // - // `; - - return <> -
- {x} -
- -}; - diff --git a/extensions/void/src/webviews/ctrlk/index.tsx b/extensions/void/src/webviews/ctrlk/index.tsx deleted file mode 100644 index 9141b713..00000000 --- a/extensions/void/src/webviews/ctrlk/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react" -import { mount } from "../common/mount" -import { CtrlK } from "./CtrlK" - -// this is the entry point that mounts ctrlk -mount() - diff --git a/extensions/void/src/webviews/diffline/DiffLine.tsx b/extensions/void/src/webviews/diffline/DiffLine.tsx deleted file mode 100644 index 6fbe8b6c..00000000 --- a/extensions/void/src/webviews/diffline/DiffLine.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useState } from 'react'; -import { useOnVSCodeMessage } from '../common/getVscodeApi'; -import { useVoidProps } from '../common/contextForProps'; - - -type props = { - text: string -} - -export const DiffLine = () => { - - const props = useVoidProps() - - console.log('props!', props) - - if (!props) { - return null - } - - // eslint-disable-next-line react/prop-types - const text = props.text - - return <> -
- {text} -
- -}; - diff --git a/extensions/void/src/webviews/diffline/index.tsx b/extensions/void/src/webviews/diffline/index.tsx deleted file mode 100644 index dc9a59b9..00000000 --- a/extensions/void/src/webviews/diffline/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react" -import { mount } from "../common/mount" -import { DiffLine } from "./DiffLine" - -// this is the entry point that mounts diffline -mount() - diff --git a/extensions/void/src/webviews/sidebar/Sidebar.tsx b/extensions/void/src/webviews/sidebar/Sidebar.tsx deleted file mode 100644 index 70bf1c3b..00000000 --- a/extensions/void/src/webviews/sidebar/Sidebar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react" -import { CodeSelection, ChatMessage, MessageToSidebar } from "../../common/shared_types" -import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi" - -import { SidebarThreadSelector } from "./SidebarThreadSelector"; -import { SidebarChat } from "./SidebarChat"; -import { SidebarSettings } from "./SidebarSettings"; -import { identifyUser } from "../common/posthog"; - - -const Sidebar = () => { - - const chatInputRef = useRef(null) - - const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat') - - // if they pressed the + to add a new chat - useOnVSCodeMessage('startNewThread', (m) => { - setTab('chat'); - chatInputRef.current?.focus(); - }) - - // ctrl+l should switch back to chat - useOnVSCodeMessage('ctrl+l', (m) => { - setTab('chat'); - chatInputRef.current?.focus(); - }) - - // if they toggled thread selector - useOnVSCodeMessage('toggleThreadSelector', (m) => { - if (tab === 'threadSelector') { - setTab('chat') - chatInputRef.current?.blur(); - } else - setTab('threadSelector') - }) - - // if they toggled settings - useOnVSCodeMessage('toggleSettings', (m) => { - if (tab === 'settings') { - setTab('chat') - chatInputRef.current?.blur(); - } else - setTab('settings') - }) - - return <> -
- -
- setTab('chat')} /> -
- -
- -
- -
- -
- -
- - -} - -export default Sidebar diff --git a/extensions/void/src/webviews/sidebar/SidebarChat.tsx b/extensions/void/src/webviews/sidebar/SidebarChat.tsx deleted file mode 100644 index cac3e295..00000000 --- a/extensions/void/src/webviews/sidebar/SidebarChat.tsx +++ /dev/null @@ -1,382 +0,0 @@ -import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react"; - - -import { marked } from 'marked'; -import MarkdownRender from "./markdown/MarkdownRender"; -import BlockCode from "./markdown/BlockCode"; -import { File, ChatMessage, CodeSelection } from "../../common/shared_types"; -import * as vscode from 'vscode' -import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi"; -import { useThreads } from "../common/contextForThreads"; -import { sendLLMMessage } from "../../common/llm"; -import { useVoidConfig } from "../common/contextForConfig"; -import { captureEvent } from "../common/posthog"; -import { generateDiffInstructions } from "../../common/systemPrompts"; - - - -const filesStr = (fullFiles: File[]) => { - return fullFiles.map(({ filepath, content }) => - ` -${filepath.fsPath} -\`\`\` -${content} -\`\`\``).join('\n') -} - -const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => { - let str = ''; - - if (files.length > 0) { - str += filesStr(files); - } - - if (selection) { - str += ` -I am currently selecting this code: -\t\`\`\`${selection.selectionStr}\`\`\` -`; - } - - if (files.length > 0 && selection) { - str += ` -Please edit the selected code or the entire file following these instructions: -`; - } else if (files.length > 0) { - str += ` -Please edit the file following these instructions: -`; - } else if (selection) { - str += ` -Please edit the selected code following these instructions: -`; - } - - str += ` -\t${instructions} -`; - if (files.length > 0) { - str += ` -\tIf you make a change, rewrite the entire file. -`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply - } - return str; -}; - - - - - -const getBasename = (pathStr: string) => { - // "unixify" path - pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with / - const parts = pathStr.split("/") // split on / - return parts[parts.length - 1] -} - -export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => { - return ( - files.length !== 0 && ( -
- {files.map((filename, i) => ( - - ))} -
- ) - ) -} - - -const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => { - - const role = chatMessage.role - const children = chatMessage.displayContent - - if (!children) - return null - - let chatbubbleContents: React.ReactNode - - if (role === 'user') { - chatbubbleContents = <> - - {chatMessage.selection?.selectionStr && } - {children} - - } - else if (role === 'assistant') { - chatbubbleContents = // sectionsHTML - } - - return
-
- {chatbubbleContents} -
-
-} - - - -export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject }) => { - - - // state of current message - const [selection, setSelection] = useState(null) // the code the user is selecting - const [files, setFiles] = useState([]) // the names of the files in the chat - const [instructions, setInstructions] = useState('') // the user's instructions - - // state of chat - const [messageStream, setMessageStream] = useState('') - const [isLoading, setIsLoading] = useState(false) - const abortFnRef = useRef<(() => void) | null>(null) - - const [latestError, setLatestError] = useState('') - - // higher level state - const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads() - - const { voidConfig } = useVoidConfig() - - - - // only captures number of messages and message "shape", no actual code, instructions, prompts, etc - const captureChatEvent = useCallback((eventId: string, extras?: object) => { - const whichApi = voidConfig.default['whichApi'] - const messages = getCurrentThread()?.messages - - captureEvent(eventId, { - whichApi: whichApi, - numMessages: messages?.length, - messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent?.length })), - version: '2024-10-19', - ...extras, - }) - }, [getCurrentThread, voidConfig.default]) - - - // if they pressed the + to add a new chat - useOnVSCodeMessage('startNewThread', (m) => { - const allThreads = getAllThreads() - // find a thread with 0 messages and switch to it - for (let threadId in allThreads) { - if (allThreads[threadId].messages.length === 0) { - switchToThread(threadId) - return - } - } - // start a new thread - startNewThread() - }) - - // if user pressed ctrl+l, add their selection to the sidebar - useOnVSCodeMessage('ctrl+l', (m) => { - setSelection(m.selection) - const filepath = m.selection.filePath - - // add current file to the context if it's not already in the files array - if (!files.find(f => f.fsPath === filepath.fsPath)) - setFiles(files => [...files, filepath]) - }) - - - const isDisabled = !instructions - - const formRef = useRef(null) - const onSubmit = async (e: FormEvent) => { - - e.preventDefault() - if (isDisabled) return - if (isLoading) return - - setIsLoading(true) - setInstructions(''); - formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens - setSelection(null) - setFiles([]) - setLatestError('') - - // request file content from vscode and await response - getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) - const relevantFiles = await awaitVSCodeResponse('files') - - // add system message to chat history - const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions } - addMessageToHistory(systemPromptElt) - - const userContent = userInstructionsStr(instructions, relevantFiles.files, selection) - const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files } - addMessageToHistory(newHistoryElt) - - captureChatEvent('Chat - Sending Message', { messageLength: instructions.length }) - const submit_time = new Date() - - // send message to LLM - sendLLMMessage({ - messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),], - onText: (newText, fullText) => setMessageStream(fullText), - onFinalMessage: (content) => { - captureChatEvent('Chat - Received Full Message', { messageLength: content.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) - - // add assistant's message to chat history, and clear selection - const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content } - addMessageToHistory(newHistoryElt) - setMessageStream('') - setIsLoading(false) - }, - onError: (error) => { - captureChatEvent('Chat - 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, } - addMessageToHistory(newHistoryElt) - setMessageStream('') - setIsLoading(false) - - setLatestError(error) - }, - voidConfig, - abortRef: abortFnRef, - }) - - - } - - const onAbort = useCallback(() => { - - captureChatEvent('Chat - Abort', { messageLengthSoFar: messageStream.length }) - - // abort claude - abortFnRef.current?.() - - // if messageStream was not empty, add it to the history - const llmContent = messageStream || '(null)' - const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, } - addMessageToHistory(newHistoryElt) - - setMessageStream('') - setIsLoading(false) - - }, [captureChatEvent, messageStream, addMessageToHistory]) - - - return <> -
- {/* previous messages */} - {getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) => - - )} - {/* message stream */} - -
- {/* chatbar */} -
- {/* selection */} -
-
-
- {/* selection */} - {(files.length || selection?.selectionStr) &&
- {/* selected files */} - - {/* selected code */} - {!!selection?.selectionStr && ( - setSelection(null)} - className="btn btn-secondary btn-sm border border-vscode-input-border rounded" - > - Remove - - )} /> - )} -
} - -
{ if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }} - - onSubmit={(e) => { - console.log('submit!') - onSubmit(e) - }}> - {/* input */} - -