├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── bun.lockb ├── docs └── vscode │ ├── extension-commands.md │ ├── extension-development-cycle.md │ └── extension-structure.md ├── images ├── greptile │ ├── ab-logo.png │ └── logo.png └── onboard │ ├── logo.png │ └── logo.svg ├── package-lock.json ├── package.json ├── src ├── credentials.ts ├── extension.ts ├── sessionManager.ts ├── types │ ├── chat.d.ts │ └── session.d.ts ├── utilities │ ├── getNonce.ts │ └── getUri.ts └── views │ ├── chatViewProvider.ts │ └── repositoryViewProvider.ts ├── tsconfig.json └── webview-ui ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.css ├── App.tsx ├── assets │ └── greptile-icon.png ├── components │ ├── chat │ │ ├── chat-list.tsx │ │ ├── chat-loading-skeleton.tsx │ │ ├── chat-message-actions.tsx │ │ ├── chat-message-sources.tsx │ │ ├── chat-message.tsx │ │ ├── chat-panel.tsx │ │ ├── chat-prompt-form.tsx │ │ ├── chat.tsx │ │ └── markdown.tsx │ ├── repo │ │ ├── chat-new-chat.tsx │ │ ├── chat-repo-chip-actions.tsx │ │ ├── chat-repo-chip.tsx │ │ └── chat-status.tsx │ └── ui │ │ ├── codeblock.tsx │ │ ├── collapsible.tsx │ │ └── dialog.tsx ├── data │ └── constants.ts ├── index.tsx ├── lib │ ├── actions.tsx │ ├── greptile-utils.ts │ ├── hooks │ │ ├── use-copy-to-clipboard.tsx │ │ └── use-enter-submit.tsx │ └── vscode-utils.ts ├── pages │ └── chat-page.tsx ├── providers │ ├── chat-state-loading-provider.tsx │ ├── chat-state-provider.tsx │ └── session-provider.tsx ├── types │ ├── chat.d.ts │ └── session.d.ts └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": "warn", 11 | "@typescript-eslint/semi": "warn", 12 | "curly": "warn", 13 | "eqeqeq": "warn", 14 | "no-throw-literal": "warn", 15 | "semi": "off" 16 | }, 17 | "ignorePatterns": ["webview-ui/**"] 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | out 3 | dist 4 | node_modules 5 | .env.local 6 | *.local 7 | greptile-*.vsix 8 | .env -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | out -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "quoteProps": "consistent", 8 | "jsxSingleQuote": true, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "always" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 23 | ], 24 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 25 | "preLaunchTask": "${defaultBuildTask}" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # This file contains all the files/directories that should 2 | # be ignored (i.e. not included) in the final packaged extension. 3 | 4 | # Ignore extension configs 5 | .vscode/** 6 | 7 | # Ignore test files 8 | .vscode-test/** 9 | out/test/** 10 | 11 | # Ignore docs 12 | docs/** 13 | 14 | # Ignore source code 15 | src/** 16 | 17 | # Ignore dependencies 18 | node_modules 19 | 20 | # Ignore all webview-ui files except the build directory 21 | webview-ui/src/** 22 | webview-ui/public/** 23 | webview-ui/scripts/** 24 | webview-ui/index.html 25 | webview-ui/README.md 26 | webview-ui/package.json 27 | webview-ui/package-lock.json 28 | webview-ui/node_modules/** 29 | 30 | # Ignore Misc 31 | .yarnrc 32 | .gitignore 33 | tsconfig.json 34 | .eslintrc.json 35 | .prettierrc 36 | .prettierignore 37 | **/vite.config.ts 38 | **/*.map 39 | **/*.ts 40 | 41 | !node_modules/@vscode/codicons/dist/codicon.css 42 | !node_modules/@vscode/codicons/dist/codicon.ttf 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Greptile AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NOTE: The Greptile VS Code extension has been **deprecated** until further notice. If you are interested in taking over the project as a maintainer, you can reach us at **hello@greptile.com**. 2 | 3 | # Greptile VS Code Extension 4 | 5 | ## Overview 6 | 7 | Greptile is a developer tool for understanding and navigating codebases in plain English. 8 | 9 | This extension for VS Code brings Greptile's capabilities directly into your development environment, enhancing productivity and code comprehension. 10 | 11 | ## Features 12 | 13 | - **Natural Language Queries:** Ask questions about your codebase in plain English. 14 | - **Supports 500+ Languages:** Work with a wide range of programming languages. 15 | - **Bug Solving:** Describe bugs and find solutions quickly. 16 | - **Legacy Code Handling:** Easily navigate and understand legacy code. 17 | - **Multi-Repository Search:** Search across multiple repositories simultaneously. 18 | 19 | ## Installation 20 | 21 | 1. Open VS Code. 22 | 2. Navigate to Extensions (Ctrl+Shift+X). 23 | 3. Search for "Greptile". 24 | 4. Click "Install". 25 | 26 | ## Usage 27 | 28 | - Press CMD+L 29 | - Type your query in natural language and press "Enter" 30 | 31 | ## Support 32 | 33 | For more information and support, visit [greptile.com](https://greptile.com). 34 | 35 | ## Feedback 36 | 37 | We value your feedback. Please submit any issues or suggestions via our [Discord](https://discord.com/invite/xZhUcFKzu7) or via [email](mailto:founders@greptile.com). 38 | 39 | --- 40 | 41 | _Greptile - Ship more features, faster._ 42 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greptileai/greptile-vscode/72fc0c5a68ff966e64c2b182a2e6bf5912410821/bun.lockb -------------------------------------------------------------------------------- /docs/vscode/extension-commands.md: -------------------------------------------------------------------------------- 1 | # Extension commands 2 | 3 | A quick run down of some of the important commands that can be run when at the root of the project. 4 | 5 | ``` 6 | npm run install:all Install package dependencies for both the extension and React webview source code. 7 | npm run start:webview Runs the React webview source code in development mode. Open http://localhost:3000 to view it in the browser. 8 | npm run build:webview Build React webview source code. Must be executed before compiling or running the extension. 9 | npm run compile Compile VS Code extension 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/vscode/extension-development-cycle.md: -------------------------------------------------------------------------------- 1 | # Extension development cycle 2 | 3 | The intended development cycle of this React-based webview extension is slightly different than that of other VS Code extensions. 4 | 5 | Due to the fact that the `webview-ui` directory holds a self-contained React application we get to take advantage of some of the perks that that enables. In particular, 6 | 7 | - UI development and iteration cycles can happen much more quickly by using Vite 8 | - Dependency management and project configuration is hugely simplified 9 | 10 | ## UI development cycle 11 | 12 | Since we can take advantage of the much faster Vite dev server, it is encouraged to begin developing webview UI by running the `npm run start:webview` command and then editing the code in the `webview-ui/src` directory. 13 | 14 | _Tip: Open the command palette and run the `Simple Browser` command and fill in `http://localhost:3000/` when prompted. This will open a simple browser environment right inside VS Code._ 15 | 16 | ### Message passing 17 | 18 | If you need to implement message passing between the webview context and extension context via the VS Code API, a helpful utility is provided in the `webview-ui/src/utilities/vscode.ts` file. 19 | 20 | This file contains a utility wrapper around the `acquireVsCodeApi()` function, which enables message passing and state management between the webview and extension contexts. 21 | 22 | This utility also enables webview code to be run in the Vite dev server by using native web browser features that mock the functionality enabled by acquireVsCodeApi. This means you can keep building your webview UI with the Vite dev server even when using the VS Code API. 23 | 24 | ### Move to traditional extension development 25 | 26 | Once you're ready to start building other parts of your extension, simply shift to a development model where you run the `npm run build:webview` command as you make changes, press `F5` to compile your extension and open a new Extension Development Host window. Inside the host window, open the command palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and type `Hello World (React + Vite): Show`. 27 | 28 | ## Dependency management and project configuration 29 | 30 | As mentioned above, the `webview-ui` directory holds a self-contained and isolated React application meaning you can (for the most part) treat the development of your webview UI in the same way you would treat the development of a regular React application. 31 | 32 | To install webview-specific dependencies simply navigate (i.e. `cd`) into the `webview-ui` directory and install any packages you need or set up any React specific configurations you want. 33 | -------------------------------------------------------------------------------- /docs/vscode/extension-structure.md: -------------------------------------------------------------------------------- 1 | # Extension structure 2 | 3 | This section provides a quick introduction into how this sample extension is organized and structured. 4 | 5 | The two most important directories to take note of are the following: 6 | 7 | - `src`: Contains all of the extension source code 8 | - `webview-ui`: Contains all of the webview UI source code 9 | 10 | ## `src` directory 11 | 12 | The `src` directory contains all of the extension-related source code and can be thought of as containing the "backend" code/logic for the entire extension. Inside of this directory you'll find the: 13 | 14 | - `panels` directory 15 | - `utilities` directory 16 | - `extension.ts` file 17 | 18 | The `panels` directory contains all of the webview-related code that will be executed within the extension context. It can be thought of as the place where all of the "backend" code for each webview panel is contained. 19 | 20 | This directory will typically contain individual TypeScript or JavaScript files that contain a class which manages the state and behavior of a given webview panel. Each class is usually in charge of: 21 | 22 | - Creating and rendering the webview panel 23 | - Properly cleaning up and disposing of webview resources when the panel is closed 24 | - Setting message listeners so data can be passed between the webview and extension 25 | - Setting the initial HTML markdown of the webview panel 26 | - Other custom logic and behavior related to webview panel management 27 | 28 | As the name might suggest, the `utilties` directory contains all of the extension utility functions that make setting up and managing an extension easier. In this case, it contains `getUri.ts` which contains a helper function which will get the webview URI of a given file or resource. 29 | 30 | Finally, `extension.ts` is where all the logic for activating and deactiving the extension usually live. This is also the place where extension commands are registered. 31 | 32 | ## `webview-ui` directory 33 | 34 | The `webview-ui` directory contains all of the React-based webview source code and can be thought of as containing the "frontend" code/logic for the extension webview. 35 | 36 | This directory is special because it contains a full-blown React application which was created using the TypeScript [Vite](https://vitejs.dev/) template. As a result, `webview-ui` contains its own `package.json`, `node_modules`, `tsconfig.json`, and so on––separate from the `hello-world` extension in the root directory. 37 | 38 | This strays a bit from other extension structures, in that you'll usually find the extension and webview dependencies, configurations, and source code more closely integrated or combined with each other. 39 | 40 | However, in this case, there are some unique benefits and reasons for why this sample extension does not follow those patterns such as easier management of conflicting dependencies and configurations, as well as the ability to use the Vite dev server, which drastically improves the speed of developing your webview UI, versus recompiling your extension code every time you make a change to the webview. 41 | -------------------------------------------------------------------------------- /images/greptile/ab-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greptileai/greptile-vscode/72fc0c5a68ff966e64c2b182a2e6bf5912410821/images/greptile/ab-logo.png -------------------------------------------------------------------------------- /images/greptile/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greptileai/greptile-vscode/72fc0c5a68ff966e64c2b182a2e6bf5912410821/images/greptile/logo.png -------------------------------------------------------------------------------- /images/onboard/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greptileai/greptile-vscode/72fc0c5a68ff966e64c2b182a2e6bf5912410821/images/onboard/logo.png -------------------------------------------------------------------------------- /images/onboard/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greptile-vscode", 3 | "displayName": "Greptile", 4 | "description": "Search and understand any repo in plain English.", 5 | "categories": ["Programming Languages", "Machine Learning", "Debuggers", "Other"], 6 | "keywords": ["search", "comprehension", "documentation", "productivity", "ai"], 7 | "publisher": "Greptile", 8 | "version": "1.0.5", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/greptileai/greptile-vscode" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/greptileai/greptile-vscode/issues" 16 | }, 17 | "icon": "images/greptile/logo.png", 18 | "galleryBanner": { 19 | "color": "#000", 20 | "theme": "dark" 21 | }, 22 | "engines": { 23 | "vscode": "^1.75.0" 24 | }, 25 | "main": "./dist/extension.js", 26 | "contributes": { 27 | "views": { 28 | "greptileView": [ 29 | { 30 | "type": "webview", 31 | "id": "repositoryView", 32 | "name": "Repositories" 33 | }, 34 | { 35 | "type": "webview", 36 | "id": "chatView", 37 | "name": "Chat" 38 | } 39 | ] 40 | }, 41 | "viewsContainers": { 42 | "activitybar": [ 43 | { 44 | "icon": "images/greptile/ab-logo.png", 45 | "id": "greptileView", 46 | "title": "Greptile" 47 | } 48 | ] 49 | }, 50 | "commands": [ 51 | { 52 | "command": "greptile.chat", 53 | "title": "Greptile: Chat" 54 | }, 55 | { 56 | "command": "greptile.signIn", 57 | "title": "Greptile: Sign In" 58 | }, 59 | { 60 | "command": "greptile.resetSession", 61 | "title": "Greptile: Reset Session" 62 | }, 63 | { 64 | "command": "greptile.resetChat", 65 | "title": "Greptile: Reset Chat", 66 | "icon": "$(clear-all)" 67 | }, 68 | { 69 | "command": "greptile.reload", 70 | "title": "Greptile: Sync Repositories", 71 | "icon": "$(repo-sync)" 72 | } 73 | ], 74 | "menus": { 75 | "view/title": [ 76 | { 77 | "command": "greptile.resetChat", 78 | "when": "view == chatView", 79 | "group": "navigation@1" 80 | }, 81 | { 82 | "command": "greptile.reload", 83 | "when": "view == chatView", 84 | "group": "navigation@0" 85 | } 86 | ] 87 | }, 88 | "keybindings": [ 89 | { 90 | "command": "greptile.chat", 91 | "key": "cmd+l", 92 | "mac": "cmd+l", 93 | "when": "isMac" 94 | }, 95 | { 96 | "command": "greptile.chat", 97 | "key": "ctrl+l", 98 | "mac": "ctrl+l", 99 | "when": "isWindows || isLinux" 100 | }, 101 | { 102 | "command": "greptile.resetChat", 103 | "key": "cmd+shift+l", 104 | "mac": "cmd+shift+l", 105 | "when": "isMac" 106 | }, 107 | { 108 | "command": "greptile.resetChat", 109 | "key": "ctrl+shift+l", 110 | "mac": "ctrl+shift+l", 111 | "when": "isWindows || isLinux" 112 | } 113 | ] 114 | }, 115 | "scripts": { 116 | "install:all": "npm install && cd webview-ui && npm install", 117 | "start:webview": "cd webview-ui && npm run start", 118 | "build:webview": "cd webview-ui && npm run build", 119 | "compile": "tsc -p ./", 120 | "watch": "tsc -watch -p ./", 121 | "pretest": "npm run compile && npm run lint", 122 | "lint": "eslint src --ext ts", 123 | "vscode:prepublish": "npm run install:all && npm run build:webview && npm run esbuild-base -- --minify", 124 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", 125 | "esbuild": "npm run esbuild-base -- --sourcemap", 126 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch", 127 | "test-compile": "tsc -p ./" 128 | }, 129 | "devDependencies": { 130 | "@types/glob": "^7.1.3", 131 | "@types/node": "^12.20.55", 132 | "@types/vscode": "^1.46.0", 133 | "@typescript-eslint/eslint-plugin": "^6.x", 134 | "@typescript-eslint/parser": "^6.x", 135 | "esbuild": "^0.20.1", 136 | "eslint": "^7.19.0", 137 | "glob": "^7.1.6", 138 | "prettier": "2.8.8", 139 | "typescript": "^5.3.3", 140 | "vscode-test": "^1.5.0" 141 | }, 142 | "dependencies": { 143 | "@octokit/rest": "^20.0.2", 144 | "@types/react": "^18.2.45", 145 | "@vscode/codicons": "^0.0.35", 146 | "ai": "^2.2.30", 147 | "posthog-js": "^1.96.1", 148 | "react": "^18.2.0" 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/credentials.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as Octokit from '@octokit/rest' 3 | import { SessionManager } from './sessionManager' 4 | import { Session } from './types/session' 5 | 6 | const GITHUB_AUTH_PROVIDER_ID = 'github' 7 | // The GitHub Authentication Provider accepts the scopes described here: 8 | // https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ 9 | const SCOPES = ['user:email', 'repo'] 10 | 11 | export class Credentials { 12 | private octokit: Octokit.Octokit | undefined 13 | 14 | async initialize(context: vscode.ExtensionContext): Promise { 15 | this.registerListeners(context) 16 | this.setOctokit() 17 | } 18 | 19 | private async setOctokit() { 20 | /** 21 | * By passing the `createIfNone` flag, a numbered badge will show up on the accounts activity bar icon. 22 | * An entry for the sample extension will be added under the menu to sign in. This allows quietly 23 | * prompting the user to sign in. 24 | * */ 25 | const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { 26 | createIfNone: false, 27 | }) 28 | 29 | if (session) { 30 | this.octokit = new Octokit.Octokit({ 31 | auth: session.accessToken, 32 | }) 33 | 34 | const email = await this.octokit.users.getAuthenticated().then((res) => { 35 | return res.data.email 36 | }) 37 | 38 | const response = await fetch('https://api.greptile.com/v1/membership', { 39 | method: 'GET', 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | 'Authorization': 'Bearer ' + session.accessToken, 43 | }, 44 | }).then(async (res) => { 45 | return res.json() 46 | }) 47 | 48 | const existingSession = SessionManager.getSession() 49 | await SessionManager.setSession({ 50 | ...existingSession, 51 | user: { 52 | ...existingSession?.user, 53 | tokens: { ['github']: { accessToken: session.accessToken } }, 54 | userId: email, 55 | membership: response['membership'], 56 | }, 57 | } as Session) 58 | // console.log(SessionManager.getSession()); 59 | return 60 | } 61 | 62 | this.octokit = undefined 63 | } 64 | 65 | registerListeners(context: vscode.ExtensionContext): void { 66 | /** 67 | * Sessions are changed when a user logs in or logs out. 68 | */ 69 | context.subscriptions.push( 70 | vscode.authentication.onDidChangeSessions(async (e) => { 71 | if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) { 72 | await this.setOctokit() 73 | } 74 | }) 75 | ) 76 | } 77 | 78 | async getOctokit(): Promise { 79 | if (this.octokit) { 80 | return this.octokit 81 | } 82 | 83 | /** 84 | * When the `createIfNone` flag is passed, a modal dialog will be shown asking the user to sign in. 85 | * Note that this can throw if the user clicks cancel. 86 | */ 87 | const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { 88 | createIfNone: true, 89 | }) 90 | this.octokit = new Octokit.Octokit({ 91 | auth: session.accessToken, 92 | }) 93 | 94 | const email = await this.octokit.users.getAuthenticated().then((res) => { 95 | return res.data.email 96 | }) 97 | 98 | const response = await fetch('https://api.greptile.com/v1/membership', { 99 | method: 'GET', 100 | headers: { 101 | 'Content-Type': 'application/json', 102 | 'Authorization': 'Bearer ' + session.accessToken, 103 | }, 104 | }).then(async (res) => { 105 | return res.json() 106 | }) 107 | 108 | const existingSession = SessionManager.getSession() 109 | await SessionManager.setSession({ 110 | ...existingSession, 111 | user: { 112 | ...existingSession?.user, 113 | tokens: { ['github']: { accessToken: session.accessToken } }, 114 | userId: email, 115 | membership: response['membership'], 116 | }, 117 | } as Session) 118 | // console.log(SessionManager.getSession); 119 | 120 | return this.octokit 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { ExtensionContext } from 'vscode' 3 | import { RepositoryViewProvider } from './views/repositoryViewProvider' 4 | import { ChatViewProvider } from './views/chatViewProvider' 5 | import { SessionManager } from './sessionManager' 6 | import { Credentials } from './credentials' 7 | import { Session } from './types/session' 8 | 9 | export async function activate(context: ExtensionContext) { 10 | SessionManager.globalState = context.globalState 11 | 12 | const credentials = new Credentials() 13 | await credentials.initialize(context) 14 | 15 | const openChat = vscode.commands.registerCommand('greptile.chat', () => { 16 | vscode.commands.executeCommand('repositoryView.focus') 17 | vscode.commands.executeCommand('chatView.focus') 18 | }) 19 | 20 | const githubAuth = vscode.commands.registerCommand('greptile.signIn', async () => { 21 | const octokit = await credentials.getOctokit() 22 | const userInfo = await octokit.users.getAuthenticated() 23 | 24 | vscode.window.showInformationMessage('Signing in with Github...') 25 | await new Promise((resolve) => setTimeout(resolve, 2000)) 26 | 27 | // reload the window to update 28 | vscode.commands.executeCommand('workbench.action.reloadWindow') 29 | }) 30 | 31 | // const signOut = vscode.commands.registerCommand('greptile.signOut', async () => { 32 | // const session = SessionManager.getSession() 33 | // SessionManager.setSession({} as Session) 34 | 35 | // vscode.window.showInformationMessage('Signed out of Greptile') 36 | // vscode.commands.executeCommand('workbench.action.reloadWindow') 37 | // }) 38 | 39 | const sessionReset = vscode.commands.registerCommand('greptile.resetSession', async () => { 40 | const session = SessionManager.getSession() 41 | SessionManager.setSession({ 42 | user: session?.user, 43 | } as Session) 44 | vscode.window.showInformationMessage('Greptile session reset') 45 | vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction') 46 | }) 47 | 48 | const chatReset = vscode.commands.registerCommand('greptile.resetChat', async () => { 49 | const session = SessionManager.getSession() 50 | SessionManager.setSession({ 51 | ...session, 52 | state: { 53 | ...session?.state, 54 | chat: null, 55 | }, 56 | } as Session) 57 | vscode.window.showInformationMessage('Greptile chat reset') 58 | vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction') 59 | }) 60 | 61 | const reload = vscode.commands.registerCommand('greptile.reload', async () => { 62 | vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction') 63 | }) 64 | 65 | const repositoryViewProvider = new RepositoryViewProvider(context.extensionUri) 66 | context.subscriptions.push( 67 | vscode.window.registerWebviewViewProvider( 68 | RepositoryViewProvider.viewType, 69 | repositoryViewProvider 70 | ) 71 | ) 72 | 73 | const chatViewProvider = new ChatViewProvider(context.extensionUri) 74 | context.subscriptions.push( 75 | vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, chatViewProvider) 76 | ) 77 | 78 | // Add command to the extension context 79 | context.subscriptions.push(openChat) 80 | context.subscriptions.push(githubAuth) 81 | // context.subscriptions.push(signOut) 82 | context.subscriptions.push(sessionReset) 83 | context.subscriptions.push(chatReset) 84 | context.subscriptions.push(reload) 85 | } 86 | -------------------------------------------------------------------------------- /src/sessionManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { Session } from './types/session' 3 | 4 | export class SessionManager { 5 | static globalState: vscode.Memento 6 | 7 | static setSession(session: Session) { 8 | return this.globalState.update('session', session) 9 | } 10 | 11 | static getSession(): Session | undefined { 12 | return this.globalState.get('session') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/chat.d.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'ai' 2 | 3 | export type Source = { 4 | repository: string 5 | remote: string 6 | branch: string 7 | filepath: string 8 | linestart: number | null 9 | lineend: number | null 10 | summary: string 11 | } 12 | 13 | export type Message = { 14 | sources?: Source[] 15 | agentStatus?: string 16 | } & Message 17 | 18 | export type RepoKey = { 19 | repository: string 20 | remote: string 21 | branch: string 22 | } 23 | 24 | type OldChatInfo = { 25 | user_id: string 26 | repo: string 27 | additional_repos: string[] 28 | session_id: string 29 | timestamp: string 30 | title: string 31 | newChat: boolean // for new sessions 32 | repos?: string[] // encoded repokey list 33 | } 34 | 35 | export type ChatInfo = { 36 | user_id: string 37 | repos: string[] // encoded repokey list 38 | session_id: string 39 | timestamp: string 40 | title: string 41 | newChat: boolean // for new sessions 42 | } 43 | 44 | export type OldChat = OldChatInfo & { 45 | chat_log: Message[] 46 | parent_id?: string 47 | } 48 | 49 | export type Chat = ChatInfo & { 50 | chat_log: Message[] 51 | parent_id?: string 52 | } 53 | 54 | export type RepositoryInfo = RepoKey & { 55 | source_id: string 56 | indexId: string 57 | filesProcessed?: number 58 | numFiles?: number 59 | message?: string 60 | private: boolean 61 | sample_questions?: string[] 62 | sha?: string 63 | external?: boolean 64 | status?: 'completed' | 'failed' | 'cloning' | 'processing' | 'submitted' | 'queued' 65 | } 66 | 67 | type ServerActionResult = Promise< 68 | | Result 69 | | { 70 | error: string 71 | } 72 | > 73 | 74 | export type SharedChatsDatabaseEntry = { 75 | id: string 76 | repositories: string[] 77 | sharedWith: string[] 78 | private: boolean 79 | owner: string 80 | } 81 | -------------------------------------------------------------------------------- /src/types/session.d.ts: -------------------------------------------------------------------------------- 1 | import { Chat, Message, RepositoryInfo } from './chat' 2 | 3 | export enum Membership { 4 | Free = 'free', 5 | Pro = 'pro', 6 | Student = 'student', 7 | } 8 | 9 | export type Session = { 10 | state?: { 11 | url: string 12 | repos: string[] 13 | repoUrl: string 14 | branch: string 15 | repoStates?: { [repo: string]: RepositoryInfo } 16 | chat?: Chat 17 | messages: Message[] 18 | isStreaming: boolean 19 | input: string 20 | } 21 | user: { 22 | userId?: string 23 | membership?: Membership | undefined 24 | checkoutSession?: string 25 | business?: boolean 26 | freeTrialDaysRemaining?: number 27 | 28 | /** Oauth access token */ 29 | tokens: ExternalTokens 30 | // the last OAuth provider used to sign in 31 | authProvider: string 32 | } 33 | } 34 | 35 | interface ExternalTokens { 36 | [key: string]: { 37 | accessToken: string 38 | idToken?: string 39 | refreshToken?: string 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/utilities/getNonce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper function that returns a unique alphanumeric identifier called a nonce. 3 | * 4 | * @remarks This function is primarily used to help enforce content security 5 | * policies for resources/scripts being executed in a webview context. 6 | * 7 | * @returns A nonce 8 | */ 9 | export function getNonce() { 10 | let text = '' 11 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 12 | for (let i = 0; i < 32; i++) { 13 | text += possible.charAt(Math.floor(Math.random() * possible.length)) 14 | } 15 | return text 16 | } 17 | -------------------------------------------------------------------------------- /src/utilities/getUri.ts: -------------------------------------------------------------------------------- 1 | import { Uri, Webview } from 'vscode' 2 | 3 | /** 4 | * A helper function which will get the webview URI of a given file or resource. 5 | * 6 | * @remarks This URI can be used within a webview's HTML as a link to the 7 | * given file/resource. 8 | * 9 | * @param webview A reference to the extension webview 10 | * @param extensionUri The URI of the directory containing the extension 11 | * @param pathList An array of strings representing the path to a file/resource 12 | * @returns A URI pointing to the file/resource 13 | */ 14 | export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { 15 | return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)) 16 | } 17 | -------------------------------------------------------------------------------- /src/views/chatViewProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { getNonce } from '../utilities/getNonce' 3 | import { getUri } from '../utilities/getUri' 4 | import { SessionManager } from '../sessionManager' 5 | 6 | export class ChatViewProvider implements vscode.WebviewViewProvider { 7 | public static readonly viewType = 'chatView' 8 | private _view?: vscode.WebviewView 9 | private eUri: vscode.Uri 10 | 11 | constructor(extensionUri: vscode.Uri) { 12 | this.eUri = extensionUri 13 | } 14 | 15 | public resolveWebviewView( 16 | webviewView: vscode.WebviewView, 17 | context: vscode.WebviewViewResolveContext, 18 | _token: vscode.CancellationToken 19 | ) { 20 | this._view = webviewView 21 | 22 | webviewView.webview.options = { 23 | enableScripts: true, 24 | localResourceRoots: [ 25 | vscode.Uri.joinPath(this.eUri, 'out'), 26 | vscode.Uri.joinPath(this.eUri, 'webview-ui/build'), 27 | vscode.Uri.joinPath(this.eUri, 'node_modules/@vscode/codicons'), 28 | ], 29 | } 30 | 31 | webviewView.webview.html = this._getHtmlForWebview(webviewView.webview) 32 | 33 | this._setWebviewMessageListener(this._view.webview) 34 | } 35 | 36 | private _getHtmlForWebview(webview: vscode.Webview) { 37 | // The CSS file from the React build output 38 | const stylesUri = getUri(webview, this.eUri, ['webview-ui', 'build', 'assets', 'index.css']) 39 | // The JS file from the React build output 40 | const scriptUri = getUri(webview, this.eUri, ['webview-ui', 'build', 'assets', 'index.js']) 41 | // VS Code codicons 42 | const codiconsUri = getUri(webview, this.eUri, [ 43 | 'node_modules', 44 | '@vscode/codicons', 45 | 'dist', 46 | 'codicon.css', 47 | ]) 48 | 49 | const nonce = getNonce() 50 | 51 | const greptileIcon = getUri(webview, this.eUri, [ 52 | 'webview-ui', 53 | 'build', 54 | 'assets', 55 | 'greptile-icon.png', 56 | ]) 57 | 58 | return /*html*/ ` 59 | 60 | 61 | 62 | 63 | 64 | 75 | 76 | 77 | Greptile 78 | 79 | 80 | 90 |
91 | 92 | 93 | 94 | ` 95 | } 96 | 97 | private _setWebviewMessageListener(webview: vscode.Webview) { 98 | webview.onDidReceiveMessage((message: any) => { 99 | const command = message.command 100 | const text = message.text 101 | 102 | switch (command) { 103 | case 'signIn': 104 | vscode.commands.executeCommand('greptile.signIn') 105 | return 106 | 107 | // case 'signOut': 108 | // vscode.commands.executeCommand('greptile.signOut') 109 | // return 110 | 111 | case 'resetChat': 112 | vscode.commands.executeCommand('greptile.resetChat') 113 | return 114 | 115 | case 'info': 116 | vscode.window.showInformationMessage(new vscode.MarkdownString(text).value) 117 | return 118 | 119 | case 'error': 120 | vscode.window.showErrorMessage(new vscode.MarkdownString(text).value) 121 | return 122 | 123 | case 'getSession': 124 | webview.postMessage({ 125 | command: 'session', 126 | value: SessionManager.getSession(), 127 | }) 128 | return 129 | 130 | case 'setSession': 131 | const session = message.session 132 | SessionManager.setSession(session) 133 | return 134 | 135 | case 'reload': 136 | vscode.commands.executeCommand('greptile.reload') 137 | return 138 | } 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/views/repositoryViewProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { getNonce } from '../utilities/getNonce' 3 | import { getUri } from '../utilities/getUri' 4 | import { SessionManager } from '../sessionManager' 5 | 6 | export class RepositoryViewProvider implements vscode.WebviewViewProvider { 7 | public static readonly viewType = 'repositoryView' 8 | private _view?: vscode.WebviewView 9 | private eUri: vscode.Uri 10 | 11 | constructor(extensionUri: vscode.Uri) { 12 | this.eUri = extensionUri 13 | } 14 | 15 | public resolveWebviewView( 16 | webviewView: vscode.WebviewView, 17 | context: vscode.WebviewViewResolveContext, 18 | _token: vscode.CancellationToken 19 | ) { 20 | this._view = webviewView 21 | 22 | webviewView.webview.options = { 23 | enableScripts: true, 24 | localResourceRoots: [ 25 | vscode.Uri.joinPath(this.eUri, 'out'), 26 | vscode.Uri.joinPath(this.eUri, 'webview-ui/build'), 27 | vscode.Uri.joinPath(this.eUri, 'node_modules/@vscode/codicons'), 28 | ], 29 | } 30 | 31 | webviewView.webview.html = this._getHtmlForWebview(webviewView.webview) 32 | 33 | this._setWebviewMessageListener(this._view.webview) 34 | } 35 | 36 | private _getHtmlForWebview(webview: vscode.Webview) { 37 | // The CSS file from the React build output 38 | const stylesUri = getUri(webview, this.eUri, ['webview-ui', 'build', 'assets', 'index.css']) 39 | // The JS file from the React build output 40 | const scriptUri = getUri(webview, this.eUri, ['webview-ui', 'build', 'assets', 'index.js']) 41 | // VS Code codicons 42 | const codiconsUri = getUri(webview, this.eUri, [ 43 | 'node_modules', 44 | '@vscode/codicons', 45 | 'dist', 46 | 'codicon.css', 47 | ]) 48 | 49 | const nonce = getNonce() 50 | 51 | return /*html*/ ` 52 | 53 | 54 | 55 | 56 | 57 | 68 | 69 | 70 | Greptile 71 | 72 | 73 |
74 | 75 | 76 | 77 | ` 78 | } 79 | 80 | private _setWebviewMessageListener(webview: vscode.Webview) { 81 | webview.onDidReceiveMessage((message: any) => { 82 | const command = message.command 83 | const text = message.text 84 | 85 | switch (command) { 86 | case 'signIn': 87 | vscode.commands.executeCommand('greptile.signIn') 88 | return 89 | 90 | // case 'signOut': 91 | // vscode.commands.executeCommand('greptile.signOut') 92 | // return 93 | 94 | case 'resetChat': 95 | vscode.commands.executeCommand('greptile.resetChat') 96 | return 97 | 98 | case 'info': 99 | vscode.window.showInformationMessage(new vscode.MarkdownString(text).value) 100 | return 101 | 102 | case 'error': 103 | vscode.window.showErrorMessage(new vscode.MarkdownString(text).value) 104 | return 105 | 106 | case 'getSession': 107 | webview.postMessage({ 108 | command: 'session', 109 | value: SessionManager.getSession(), 110 | }) 111 | return 112 | 113 | case 'setSession': 114 | const session = message.session 115 | SessionManager.setSession(session) 116 | return 117 | 118 | case 'reload': 119 | vscode.commands.executeCommand('greptile.reload') 120 | return 121 | } 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "esModuleInterop": true, 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": false, 11 | "skipLibCheck": true 12 | }, 13 | "exclude": ["node_modules", ".vscode-test", "webview-ui"] 14 | } 15 | -------------------------------------------------------------------------------- /webview-ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | build 6 | build-ssr 7 | .env.local 8 | *.local -------------------------------------------------------------------------------- /webview-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Greptile 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /webview-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greptile", 3 | "version": "1.0.2", 4 | "private": true, 5 | "scripts": { 6 | "start": "vite", 7 | "dev": "vite dev", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-collapsible": "^1.0.3", 13 | "@radix-ui/react-dialog": "^1.0.5", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@vscode/webview-ui-toolkit": "^1.2.2", 16 | "ai": "^2.2.11", 17 | "axios": "^1.5.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.10", 20 | "js-base64": "^3.7.5", 21 | "js-tiktoken": "^1.0.7", 22 | "lucide-react": "^0.270.0", 23 | "openai": "^4.0.0", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-markdown": "^8.0.7", 27 | "react-router-dom": "^6.20.0", 28 | "react-syntax-highlighter": "^15.5.0" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20.4.2", 32 | "@types/react": "18.2.45", 33 | "@types/react-dom": "18.2.7", 34 | "@types/react-syntax-highlighter": "^15.5.7", 35 | "@types/vscode-webview": "^1.57.0", 36 | "@vitejs/plugin-react": "^1.0.7", 37 | "prettier": "^3.0.0", 38 | "typescript": "^5.1.3", 39 | "vite": "^2.9.13" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webview-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greptileai/greptile-vscode/72fc0c5a68ff966e64c2b182a2e6bf5912410821/webview-ui/public/favicon.ico -------------------------------------------------------------------------------- /webview-ui/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: var(--vscode-foreground); 3 | font-size: var(--vscode-font-size); 4 | font-weight: var(--vscode-font-weight); 5 | font-family: var(--vscode-font-family); 6 | } 7 | 8 | main { 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: flex-start; 13 | height: 100%; 14 | margin: 1rem 0; 15 | } 16 | 17 | a { 18 | color: var(--vscode-textLink-foreground); 19 | text-decoration: none; 20 | } 21 | 22 | a:hover, 23 | a:active { 24 | color: var(--vscode-textLink-activeForeground); 25 | /* color: #388bfd; */ 26 | } 27 | 28 | code { 29 | font-size: var(--vscode-editor-font-size); 30 | font-family: var(--vscode-editor-font-family); 31 | } 32 | 33 | /* Webview UI Toolkit */ 34 | 35 | .divider { 36 | border-color: var(--vscode-activityBar-border); 37 | width: 100%; 38 | margin: 0; 39 | } 40 | 41 | /* Chat Panel */ 42 | 43 | #chat-prompt { 44 | display: flex; 45 | flex-direction: row; 46 | align-items: center; 47 | } 48 | 49 | #chat-input { 50 | margin-top: 0.75rem; 51 | margin-bottom: 0.5rem; 52 | padding-right: 0.75rem; 53 | } 54 | 55 | #chat-submit { 56 | height: 2.2rem; 57 | } 58 | 59 | .secondary-button { 60 | margin-right: 0.75rem; 61 | margin-bottom: 1rem; 62 | } 63 | 64 | /* Chat Message */ 65 | 66 | .message-container { 67 | display: flex; 68 | flex-direction: column; 69 | position: relative; 70 | align-items: flex-start; 71 | padding-bottom: 0.25rem; 72 | } 73 | 74 | .message-header, 75 | .message-body { 76 | display: flex; 77 | flex-direction: row; 78 | align-items: center; 79 | justify-content: space-between; 80 | width: 100%; 81 | } 82 | 83 | .role { 84 | display: flex; 85 | flex-direction: row; 86 | align-items: center; 87 | justify-content: flex-start; 88 | gap: 0.75rem; 89 | font-weight: 700; 90 | } 91 | 92 | .role-icon { 93 | display: flex; 94 | flex-shrink: 0; 95 | user-select: none; 96 | align-items: center; 97 | border: solid; 98 | border-color: var(--vscode-activityBar-border); 99 | border-radius: 0.375rem; 100 | border-width: 1px; 101 | padding: 0.25rem; 102 | } 103 | 104 | .greptile-icon { 105 | background-image: url(./assets/greptile-icon.png); 106 | background-size: contain; 107 | background-repeat: no-repeat; 108 | background-position: center; 109 | height: 16px; 110 | width: 16px; 111 | } 112 | 113 | /* .role-title { 114 | font-size: calc(var(--vscode-font-size) * 1.2); 115 | } */ 116 | 117 | .message-content { 118 | flex: 1 1 0%; 119 | padding: 0 0.25rem 0.25rem 0.25rem; 120 | line-height: 1.25rem; 121 | } 122 | 123 | .agent-status { 124 | margin-top: 0.5rem; 125 | font-weight: bold; 126 | } 127 | 128 | .code-copy { 129 | display: flex; 130 | flex-direction: row; 131 | align-items: center; 132 | } 133 | 134 | .code-language { 135 | font-size: var(--vscode-editor-font-size); 136 | font-family: var(--vscode-editor-font-family); 137 | } 138 | 139 | .source { 140 | display: flex; 141 | flex-direction: row; 142 | align-items: center; 143 | } 144 | 145 | /* Sample Repos */ 146 | 147 | #sample-repos { 148 | box-sizing: border-box; 149 | display: flex; 150 | flex-flow: column nowrap; 151 | align-items: flex-start; 152 | justify-content: flex-start; 153 | margin: 0.5rem 0 1rem 0; 154 | } 155 | 156 | #sample-repos label { 157 | display: block; 158 | color: var(--vscode-foreground); 159 | cursor: pointer; 160 | font-size: var(--vscode-font-size); 161 | line-height: normal; 162 | margin-bottom: 1rem; 163 | } 164 | 165 | /* New Repos */ 166 | 167 | #new-repo-container { 168 | display: flex; 169 | flex-direction: row; 170 | align-items: end; 171 | } 172 | #new-repo-branch-submit { 173 | display: flex; 174 | align-items: end; 175 | } 176 | #new-repo-branch { 177 | margin-left: 0.5rem; 178 | } 179 | 180 | #new-repo-submit { 181 | /* align-self: flex-end; */ 182 | margin-left: 1rem; 183 | } 184 | 185 | /* Loading Skeleton */ 186 | 187 | .skeleton-container { 188 | display: flex; 189 | flex-direction: column; 190 | margin-top: 0.5rem; 191 | gap: 2px; 192 | } 193 | 194 | .skeleton { 195 | height: 1rem; 196 | background: var(--vscode-list-hoverBackground); 197 | border-radius: 3px; 198 | animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 199 | } 200 | 201 | .skeleton.full { 202 | width: 100%; 203 | } 204 | 205 | .skeleton.part { 206 | width: 75%; 207 | } 208 | 209 | @keyframes pulse { 210 | 0%, 211 | 100% { 212 | opacity: 1; 213 | } 214 | 50% { 215 | opacity: 0.5; 216 | } 217 | } 218 | 219 | /* Processing Status */ 220 | 221 | .processing-title { 222 | display: flex; 223 | flex-direction: row; 224 | align-items: center; 225 | gap: 1rem; 226 | padding: 0.5rem 0.5rem 0.5rem 0; 227 | } 228 | 229 | .processing-body { 230 | display: flex; 231 | flex-direction: column; 232 | padding: 0.5rem 1rem; 233 | } 234 | 235 | .processing-grid { 236 | width: 100%; 237 | display: grid; 238 | grid-template-columns: repeat(12, minmax(0, 1fr)); 239 | padding-bottom: 2px; 240 | } 241 | 242 | .processing-status { 243 | grid-column: span 5 / span 5; 244 | } 245 | 246 | .text-red { 247 | color: red; 248 | } 249 | 250 | .text-yellow { 251 | color: yellow; 252 | } 253 | 254 | .text-green { 255 | color: green; 256 | } 257 | 258 | .text-gray { 259 | color: gray; 260 | } 261 | 262 | .repo-chip { 263 | display: flex; 264 | flex-direction: row; 265 | align-items: center; 266 | gap: 0.5rem; 267 | position: relative; 268 | } 269 | 270 | #repo-chips { 271 | /* padding-top: 2rem; */ 272 | margin-top: 2rem; 273 | padding-bottom: 1rem; 274 | } 275 | 276 | /* Progress Bar */ 277 | 278 | .circular-progress-bar { 279 | vertical-align: top; 280 | } 281 | 282 | .circular-progress-bar .outline { 283 | fill: transparent; 284 | stroke: var(--vscode-list-hoverBackground); 285 | } 286 | 287 | .circular-progress-bar .fill { 288 | fill: transparent; 289 | stroke: var(--vscode-progressBar-background); 290 | transition: 'stroke-dashoffset 0.35s'; 291 | } 292 | 293 | .retry-button { 294 | margin-top: 0.5rem; 295 | } 296 | 297 | /* Sign-in */ 298 | 299 | #sign-in-container { 300 | display: flex; 301 | justify-content: center; 302 | align-items: center; 303 | height: 100vh; 304 | } 305 | 306 | .sign-in-icon { 307 | padding-right: 0.5rem; 308 | } 309 | 310 | /* Other */ 311 | 312 | .sr-only { 313 | position: absolute; 314 | width: 1px; 315 | height: 1px; 316 | padding: 0; 317 | margin: -1px; 318 | overflow: hidden; 319 | clip: rect(0, 0, 0, 0); 320 | white-space: nowrap; 321 | border-width: 0; 322 | } 323 | 324 | .icon { 325 | font-size: var(--vscode-font-size); 326 | } 327 | 328 | .chat-page { 329 | margin-top: 0.5rem; 330 | } 331 | -------------------------------------------------------------------------------- /webview-ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { createMemoryRouter, RouterProvider, Navigate } from 'react-router-dom' 3 | import { NewChat } from './components/repo/chat-new-chat' 4 | import ChatPage from './pages/chat-page' 5 | import { vscode } from './lib/vscode-utils' 6 | import { SessionContext } from './providers/session-provider' 7 | import type { Session } from './types/session' 8 | 9 | import './App.css' 10 | import { usePostHog } from 'posthog-js/react' 11 | 12 | export interface AppProps { 13 | viewType: string 14 | } 15 | 16 | function App({ viewType }: AppProps) { 17 | // console.log("Starting App") 18 | 19 | if (!viewType) return 20 | 21 | const router = createMemoryRouter([ 22 | { 23 | path: 'repositoryView', 24 | element: , 25 | }, 26 | { 27 | path: 'chatView', 28 | element: , 29 | }, 30 | { 31 | path: '*', 32 | element: , 33 | }, 34 | ]) 35 | 36 | const [session, setSession] = useState(undefined) 37 | const posthog = usePostHog() 38 | 39 | // useEffect(() => { 40 | // console.log('Navigated to', window.location) 41 | // }, [window?.location]) 42 | 43 | useEffect(() => { 44 | // write session to extension 45 | // console.log('Writing session to extension', session) 46 | if (!session) return 47 | vscode.postMessage({ 48 | command: 'setSession', 49 | session, 50 | }) 51 | }, [session]) 52 | 53 | useEffect(() => { 54 | // console.log('Identifying user', session?.user?.userId) 55 | if (session?.user?.userId) { 56 | posthog.identify(session?.user?.userId, { 57 | email: session?.user?.userId, 58 | membership: 59 | session?.user?.membership === 'pro' && session?.user?.business 60 | ? 'business' 61 | : session?.user?.membership, 62 | }) 63 | } 64 | }, [posthog, session?.user]) 65 | 66 | useEffect(() => { 67 | // console.log('Setting up event listener') 68 | const eventListener = async (event) => { 69 | const message = event.data 70 | switch (message.command) { 71 | case 'session': 72 | // console.log('Loaded session from extension', message.value) 73 | setSession(message.value) 74 | } 75 | } 76 | 77 | // listen for response from extension 78 | window.addEventListener('message', eventListener) 79 | 80 | // console.log('Requesting session from extension') 81 | // make call to extension 82 | vscode.postMessage({ 83 | command: 'getSession', 84 | text: '', 85 | }) 86 | 87 | // unhook listener 88 | return () => window.removeEventListener('message', eventListener) 89 | }, []) 90 | 91 | return ( 92 | 93 | 94 | 95 | ) 96 | } 97 | 98 | export default App 99 | -------------------------------------------------------------------------------- /webview-ui/src/assets/greptile-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greptileai/greptile-vscode/72fc0c5a68ff966e64c2b182a2e6bf5912410821/webview-ui/src/assets/greptile-icon.png -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeDivider } from '@vscode/webview-ui-toolkit/react' 2 | 3 | import { ChatMessage } from './chat-message' 4 | import { ChatLoadingSkeleton } from './chat-loading-skeleton' 5 | import { useChatState } from '../../providers/chat-state-provider' 6 | import { Message } from '../../types/chat' 7 | import { Session } from '../../types/session' 8 | 9 | export interface ChatListProps { 10 | session: Session | null 11 | messages: Message[] 12 | isLoading: boolean 13 | isStreaming: boolean 14 | readonly?: boolean 15 | setMessages: (messages: Message[]) => void 16 | sessionId: string 17 | } 18 | 19 | export function ChatList({ 20 | session, 21 | messages, 22 | isLoading, 23 | isStreaming, 24 | readonly = false, 25 | setMessages, 26 | sessionId, 27 | }: ChatListProps) { 28 | const { chatState } = useChatState() 29 | if (!messages.length) { 30 | return null 31 | } 32 | 33 | const deleteMessage = (index: number) => { 34 | // delete messages at index (query) and index + 1 (response) 35 | const newMessages = messages.slice(0, index).concat(messages.slice(index + 2)) 36 | setMessages(newMessages) 37 | 38 | // todo: send api request to update backend 39 | } 40 | 41 | return ( 42 |
43 | {messages.map((message, index) => { 44 | return ( 45 |
46 | { 54 | deleteMessage(index) 55 | }} 56 | /> 57 |
58 | ) 59 | })} 60 | {isLoading && !isStreaming && ( 61 | <> 62 | 63 | 64 | 65 | )} 66 | {chatState.disabled.value && !isStreaming && !isLoading && ( 67 |

Please Sign In to Continue

68 | )} 69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat-loading-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export const ChatLoadingSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | import { usePostHog } from 'posthog-js/react' 3 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react' 4 | 5 | import { useCopyToClipboard } from '../../lib/hooks/use-copy-to-clipboard' 6 | 7 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 8 | message: Message 9 | userId?: string 10 | readonly?: boolean 11 | deleteMessage: () => void 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | userId, 17 | className, 18 | readonly = false, 19 | deleteMessage, 20 | ...props 21 | }: ChatMessageActionsProps) { 22 | const posthog = usePostHog() 23 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 24 | 25 | const onCopy = () => { 26 | if (isCopied) return 27 | copyToClipboard(message.content) 28 | } 29 | 30 | return ( 31 |
32 | 33 | {isCopied ? ( 34 |
35 | ) : ( 36 |
37 | )} 38 | Copy message 39 |
40 | {/* {message.role == 'assistant' && !readonly && ( 41 | <> 42 | { 46 | posthog.capture('Feedback', { 47 | user: userId!, 48 | feedback: 'thumbs up', 49 | source: 'greptile-vscode', 50 | }) 51 | }} 52 | > 53 |
54 |
55 | { 59 | posthog.capture('Feedback', { 60 | user: userId!, 61 | feedback: 'thumbs down', 62 | source: 'greptile-vscode', 63 | }) 64 | }} 65 | > 66 |
67 |
68 | 69 | )} */} 70 | {/* {message.role === 'user' && !readonly && ( 71 | <> 72 | 73 |
74 |
75 | 76 | )} */} 77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat-message-sources.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { ChatLoadingSkeleton } from './chat-loading-skeleton' 4 | import { getRepoUrlForAction } from '../../lib/greptile-utils' 5 | import { Source, RepositoryInfo } from '../../types/chat' 6 | 7 | interface IChatMessageSources { 8 | sources: Source[] | undefined 9 | repoStates: { [repoKey: string]: RepositoryInfo } 10 | isLoading: boolean 11 | } 12 | 13 | export const ChatMessageSources = ({ sources, repoStates, isLoading }: IChatMessageSources) => { 14 | const [expandedSources, setExpandedSources] = React.useState(false) 15 | const numberOfSourcesToDisplayWhenCollapsed = 3 16 | const skeletonArray = Array.from(Array(numberOfSourcesToDisplayWhenCollapsed + 1).keys()) 17 | 18 | const getURL = (repoKey: string, repoState: RepositoryInfo, source: Source) => { 19 | if (repoState?.external) return `https://${repoKey}${source?.filepath}` 20 | 21 | return getRepoUrlForAction( 22 | { 23 | repository: repoState?.repository, 24 | branch: repoState?.branch, 25 | remote: repoState?.remote, 26 | }, 27 | 'source', 28 | { 29 | filepath: source?.filepath, 30 | lines: 31 | source?.linestart && source?.lineend ? [source?.linestart, source?.lineend] : undefined, 32 | } 33 | ) 34 | } 35 | const sourcesCount = sources?.length || 0 36 | if (!sources || sourcesCount === 0) return
37 | 38 | return ( 39 |
40 |

{sourcesCount} result(s)

41 | {(expandedSources ? sources : sources.slice(0, numberOfSourcesToDisplayWhenCollapsed)).map( 42 | (source: Source, index: number) => { 43 | const remote = source?.remote || 'github' 44 | const branch = source?.branch || 'main' 45 | const repo = source?.repository 46 | 47 | const urlRepoKey = `${remote}:${branch}:${repo}` 48 | const repoKey = `${remote}:${repo}:${branch}` 49 | 50 | return ( 51 | 66 | ) 67 | } 68 | )} 69 | 70 | {(expandedSources || 71 | (!expandedSources && sources.length > numberOfSourcesToDisplayWhenCollapsed)) && ( 72 |
setExpandedSources(!expandedSources)}> 73 |
74 | {expandedSources ? ( 75 |
76 | ) : ( 77 |
78 | )} 79 | {expandedSources 80 | ? `Collapse` 81 | : `View ${sources.length - numberOfSourcesToDisplayWhenCollapsed} More...`} 82 |
83 |
84 | )} 85 | 86 | {/* {skeletonArray.map((i) => { 87 | if (isLoading && sources.length < i + 1) { 88 | return // ; 89 | } 90 | })} */} 91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat-message.tsx: -------------------------------------------------------------------------------- 1 | // Inspired by Chatbot-UI and modified to fit the needs of this project 2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx 3 | 4 | import { useEffect, useState } from 'react' 5 | import { VSCodeDivider } from '@vscode/webview-ui-toolkit/react' 6 | 7 | import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialog' 8 | import { MemoizedReactMarkdown } from './markdown' 9 | import { CodeBlock } from '../ui/codeblock' 10 | import { ChatMessageSources } from './chat-message-sources' 11 | import { ChatMessageActions } from './chat-message-actions' 12 | import { Message, RepositoryInfo } from '../../types/chat' 13 | 14 | import '../../App.css' 15 | 16 | export interface ChatMessageProps { 17 | userId: string 18 | message: Message 19 | repoStates: { [repo: string]: RepositoryInfo } 20 | readonly?: boolean 21 | displayDivider?: boolean 22 | firstMessage?: boolean 23 | deleteMessage: () => void 24 | } 25 | 26 | export function ChatMessage({ 27 | userId, 28 | message, 29 | repoStates, 30 | readonly = false, 31 | displayDivider = false, 32 | firstMessage = false, 33 | deleteMessage, 34 | ...props 35 | }: ChatMessageProps) { 36 | const [sourcesLoading, setSourcesLoading] = useState(true) 37 | 38 | useEffect(() => { 39 | if (message?.agentStatus?.includes('response') || message.content) { 40 | setSourcesLoading(false) 41 | } 42 | }, [message?.agentStatus]) 43 | 44 | return ( 45 |
46 |
47 | {message.role === 'assistant' && ( 48 |
49 |
50 |
Greptile
51 |
52 | )} 53 | {message.role === 'assistant' && !firstMessage && ( 54 |
55 | 61 |
62 | )} 63 |
64 |
65 |
66 | {message.agentStatus &&
{message.agentStatus}
} 67 |
68 | 73 | {message.content ? ( 74 | {children}

78 | }, 79 | h1({ children }) { 80 | return

{children}

81 | }, 82 | h2({ children }) { 83 | return

{children}

84 | }, 85 | h3({ children }) { 86 | return

{children}

87 | }, 88 | code({ node, inline, className, children, ...props }) { 89 | if (children.length) { 90 | if (children[0] == '▍') { 91 | return 92 | } 93 | 94 | children[0] = (children[0] as string).replace('`▍`', '▍') 95 | } 96 | 97 | const match = /language-(\w+)/.exec(className || '') 98 | 99 | if (inline) { 100 | return {children} 101 | } 102 | return ( 103 | 109 | ) 110 | }, 111 | ol({ children }: any) { 112 | return
    {children}
113 | }, 114 | ul({ children }: any) { 115 | return
    {children}
116 | }, 117 | img({ src, alt }: any) { 118 | return ( 119 | 120 | 121 |
122 | {alt!} 123 |
124 | 133 | 134 | 139 | 140 | {' '} 141 | 142 | {' '} 143 | 144 | {' '} 145 | 146 | {' '} 147 | {' '} 148 | {' '} 149 | {' '} 150 | {' '} 151 | {' '} 152 | 153 | 154 |
155 |
156 |
157 | 158 | 159 | {alt!} 160 | 161 | 162 |
163 | ) 164 | }, 165 | a({ href, children }) { 166 | return ( 167 | 168 | {children} 169 | 170 | ) 171 | }, 172 | }} 173 | > 174 | {message.role === 'assistant' && !message.sources && message.content[0] === '[' 175 | ? '' 176 | : message.content.replaceAll('\u200b', '')} 177 |
178 | ) : ( 179 |
{/* Loading Skeleton */}
180 | )} 181 |
182 |
183 | {message.role === 'user' && ( 184 |
185 | 191 |
192 | )} 193 |
194 | {displayDivider && } 195 |
196 | ) 197 | } 198 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react' 3 | import { usePostHog } from 'posthog-js/react' 4 | 5 | import { PromptForm } from './chat-prompt-form' 6 | import { useChatState } from '../../providers/chat-state-provider' 7 | import { Message } from '../../types/chat' 8 | 9 | import '../../App.css' 10 | 11 | export interface ChatPanelProps { 12 | messages: Message[] 13 | isLoading: boolean 14 | isStreaming: boolean 15 | someValidRepos: boolean 16 | input: string 17 | sessionId: string 18 | setInput: (input: string) => void 19 | setIsStreaming: (isStreaming: boolean) => void 20 | stop: () => void 21 | append: (message: Message) => void 22 | reload: () => void 23 | } 24 | 25 | export function ChatPanel({ 26 | messages, 27 | isLoading, 28 | isStreaming, 29 | someValidRepos, 30 | input, 31 | sessionId, 32 | setInput, 33 | setIsStreaming, 34 | stop, 35 | append, 36 | reload, 37 | }: ChatPanelProps) { 38 | const { chatState } = useChatState() 39 | const posthog = usePostHog() 40 | 41 | const messagesEndRef = useRef(null) 42 | 43 | // Scroll to bottom function 44 | const scrollToBottom = () => { 45 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) 46 | } 47 | 48 | // Effect to scroll to bottom on mount 49 | useEffect(() => { 50 | scrollToBottom() 51 | }, []) 52 | 53 | return ( 54 |
55 | { 57 | console.log('Chat message sent', value) 58 | posthog.capture('Chat message sent', { 59 | source: 'greptile-vscode', 60 | }) 61 | await append({ 62 | id: chatState.session_id, 63 | content: value, 64 | role: 'user', 65 | }) 66 | }} 67 | input={input} 68 | setInput={setInput} 69 | isLoading={isLoading} 70 | isStreaming={isStreaming} 71 | someValidRepos={someValidRepos} 72 | renderButton={() => 73 | isLoading ? ( 74 | { 78 | posthog.capture('Response stopped', { 79 | source: 'greptile-vscode', 80 | }) 81 | stop() 82 | }} 83 | className='secondary-button' 84 | > 85 | Stop generating 86 | 87 | ) : ( 88 | messages?.length > 2 && ( 89 | { 93 | posthog.capture('Response regenerated', { 94 | source: 'greptile-vscode', 95 | }) 96 | reload() 97 | }} 98 | className='secondary-button' 99 | > 100 | Regenerate response 101 | 102 | ) 103 | ) 104 | } 105 | /> 106 | {/*
*/} 107 |
108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat-prompt-form.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useRef, useEffect } from 'react' 2 | import { VSCodeButton, VSCodeTextArea } from '@vscode/webview-ui-toolkit/react' 3 | 4 | import { useChatState } from '../../providers/chat-state-provider' 5 | import { useEnterSubmit } from '../../lib/hooks/use-enter-submit' 6 | 7 | export interface PromptProps { 8 | input: string 9 | setInput: (input: string) => void 10 | onSubmit: (value: string) => Promise 11 | isLoading: boolean 12 | isStreaming: boolean 13 | renderButton: () => ReactElement<{}> 14 | someValidRepos: boolean 15 | } 16 | 17 | export function PromptForm({ 18 | onSubmit, 19 | input, 20 | setInput, 21 | isLoading, 22 | isStreaming, 23 | renderButton, 24 | someValidRepos, 25 | }: PromptProps) { 26 | const { formRef, onKeyDown } = useEnterSubmit() 27 | const textAreaRef = useRef(null) 28 | 29 | const { chatState } = useChatState() 30 | 31 | useEffect(() => { 32 | if (textAreaRef.current) { 33 | textAreaRef.current.focus() 34 | } 35 | }, []) 36 | return ( 37 |
{ 40 | // if (submitDisabled) return; 41 | e.preventDefault() 42 | setInput('') 43 | if (!input?.trim()) { 44 | return 45 | } 46 | await onSubmit(input) 47 | }} 48 | ref={formRef} 49 | > 50 |
51 |
52 | setInput(e.target.value)} 64 | placeholder={ 65 | someValidRepos ? 'Ask a question' : 'Please wait while we process your repositories' 66 | } 67 | spellCheck={false} 68 | id='chat-input' 69 | /> 70 | 77 |
78 |
79 |
80 |
{renderButton()}
81 |
82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/chat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react' 2 | import { useChat } from 'ai/react' 3 | 4 | import { useChatState } from '../../providers/chat-state-provider' 5 | import { SessionContext } from '../../providers/session-provider' 6 | import { cleanMessage } from '../../lib/greptile-utils' 7 | import { vscode } from '../../lib/vscode-utils' 8 | import { Message, RepositoryInfo } from '../../types/chat' 9 | import { Session } from '../../types/session' 10 | import { API_BASE } from '../../data/constants' 11 | import { ChatList } from './chat-list' 12 | import { ChatPanel } from './chat-panel' 13 | 14 | export interface ChatProps extends React.ComponentProps<'div'> { 15 | initialMessages: Message[] 16 | sessionId: string 17 | repoStates: { [repo: string]: RepositoryInfo } 18 | } 19 | 20 | export const Chat = function ChatComponent({ initialMessages, sessionId, repoStates }: ChatProps) { 21 | // console.log("Starting Chat", session_id, repoStates, initialMessages); 22 | 23 | const { session, setSession } = useContext(SessionContext) 24 | 25 | const [displayMessages, setDisplayMessages] = useState([]) 26 | const [isStreaming, setIsStreaming] = useState(false) 27 | const { chatState, chatStateDispatch } = useChatState() 28 | 29 | let repos = Object.values(session?.state?.repoStates).map((repoState: RepositoryInfo) => ({ 30 | name: repoState.repository, 31 | branch: repoState.branch, 32 | })) 33 | 34 | // vercel's ai useChat 35 | const { messages, input, isLoading, append, reload, stop, setInput, setMessages } = useChat({ 36 | api: `${API_BASE}/query`, 37 | initialMessages, 38 | id: sessionId, 39 | headers: { 40 | Authorization: 'Bearer ' + session?.user?.tokens?.github.accessToken, 41 | }, 42 | body: { 43 | repositories: repos, 44 | initialMessages, 45 | sessionId: sessionId, 46 | }, 47 | async onResponse(response) { 48 | setSession({ 49 | ...session, 50 | state: { 51 | ...session?.state, 52 | isStreaming: true, 53 | }, 54 | } as Session) 55 | setIsStreaming(true) 56 | if (response.status === 401 || response.status === 500) { 57 | console.log(`${response.status} Chat Error`) 58 | vscode.postMessage({ 59 | command: 'error', 60 | text: 'Chat Error - Please reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 61 | }) 62 | } else if (response.status === 404) { 63 | // && session?.user?.refreshToken) { 64 | console.log('Error: Needs refresh or unauthorized') 65 | // todo: refresh and reload 66 | } 67 | }, 68 | onFinish(message) { 69 | setSession({ 70 | ...session, 71 | state: { 72 | ...session?.state, 73 | isStreaming: false, 74 | }, 75 | } as Session) 76 | setIsStreaming(false) 77 | }, 78 | }) 79 | 80 | useEffect(() => { 81 | const newDisplayMessages = messages.map((message) => cleanMessage(message)) 82 | setDisplayMessages(newDisplayMessages) 83 | if (messages.length === 0) return 84 | if (!session?.user && messages.length > 2) { 85 | // console.log('Max messages reached') 86 | chatStateDispatch({ 87 | action: 'set_disabled', 88 | payload: { 89 | value: true, 90 | reason: 'Please sign in to continue.', 91 | }, 92 | }) 93 | return 94 | } else { 95 | chatStateDispatch({ 96 | action: 'set_disabled', 97 | payload: { 98 | value: false, 99 | reason: '', 100 | }, 101 | }) 102 | } 103 | }, [messages, session?.user]) 104 | 105 | const someValidRepos = Object.values(session?.state?.repoStates).some( 106 | (repoState: RepositoryInfo) => { 107 | // console.log('repo state: ', repoState) 108 | return (repoState.status !== 'completed' && repoState.sha) || repoState.status === 'completed' 109 | } 110 | ) 111 | 112 | return ( 113 |
114 | {someValidRepos ? ( 115 |
116 | 124 | {displayMessages.length <= 1 && 125 | Object.keys(session?.state?.repoStates).length > 0 && 126 | !isLoading &&
{/* Sample Questions */}
} 127 |
128 | ) : ( 129 |
130 |
131 |

132 | We will email you at {session?.user?.userId} once your repository has finished 133 | processing. 134 |

135 |
136 |
137 | )} 138 | 151 |
152 | ) 153 | } 154 | -------------------------------------------------------------------------------- /webview-ui/src/components/chat/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && prevProps.className === nextProps.className 8 | ) 9 | -------------------------------------------------------------------------------- /webview-ui/src/components/repo/chat-new-chat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react' 2 | import { VSCodeTextField, VSCodeButton } from '@vscode/webview-ui-toolkit/react' 3 | import { usePostHog } from 'posthog-js/react' 4 | 5 | import { API_BASE } from '../../data/constants' 6 | import { vscode } from '../../lib/vscode-utils' 7 | import { deserializeRepoKey, parseRepoInput } from '../../lib/greptile-utils' 8 | import { SessionContext } from '../../providers/session-provider' 9 | import type { Session } from '../../types/session' 10 | import { RepositoryInfo } from '../../types/chat' 11 | import { ChatStatus } from './chat-status' 12 | import { RepoChip } from './chat-repo-chip' 13 | import { RepoChipActions } from './chat-repo-chip-actions' 14 | 15 | import '../../App.css' 16 | import { ChatLoadingStateProvider } from '../../providers/chat-state-loading-provider' 17 | 18 | export const NewChat = () => { 19 | const posthog = usePostHog() 20 | 21 | const { session, setSession } = useContext(SessionContext) 22 | const [isCloning, setIsCloning] = useState(false) 23 | 24 | const handleClone = async () => { 25 | setIsCloning(true) 26 | 27 | console.log('Parsing user input') 28 | const parsedRepo = await parseRepoInput(session) 29 | if (!parsedRepo) { 30 | console.log('Error: Invalid repository identifier') 31 | vscode.postMessage({ 32 | command: 'error', 33 | text: 'Error: Invalid repository identifier', 34 | }) 35 | setIsCloning(false) 36 | return 37 | } 38 | 39 | // console.log('Checking membership') 40 | const checkMembership = async () => { 41 | if (!session?.user) return 42 | 43 | const response = await fetch(`${API_BASE}/membership`, { 44 | method: 'GET', 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 48 | }, 49 | }).then(async (res) => { 50 | return res.json() 51 | }) 52 | 53 | if (response['membership'] !== session?.user?.membership) { 54 | setSession({ 55 | ...session, 56 | user: { 57 | ...session?.user, 58 | token: session?.user?.tokens?.github.accessToken, 59 | membership: response['membership'], 60 | }, 61 | } as Session) 62 | } 63 | } 64 | checkMembership() 65 | 66 | if (session?.user?.membership !== 'pro') { 67 | vscode.postMessage({ 68 | command: 'info', 69 | text: `Upgrade to pro to use this extension!`, 70 | }) 71 | setIsCloning(false) 72 | return 73 | } 74 | 75 | console.log('Handling clone') 76 | const submitJob = async () => { 77 | console.log('Submitting ', parsedRepo) 78 | 79 | const dRepoKey = deserializeRepoKey(parsedRepo) 80 | 81 | return fetch(`${API_BASE}/repositories`, { 82 | method: 'POST', 83 | body: JSON.stringify({ 84 | remote: dRepoKey.remote, 85 | repository: dRepoKey.repository.toLowerCase() || '', 86 | branch: dRepoKey.branch.toLowerCase() || '', 87 | }), 88 | headers: { 89 | 'Content-Type': 'application/json', 90 | 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 91 | }, 92 | }).then(async (res) => { 93 | if (res.ok) { 94 | // console.log('yay'); 95 | vscode.postMessage({ 96 | command: 'info', 97 | text: `Repository submitted. If this is a new repository, we will email you at ${session?.user?.userId} once it has finished processing.`, 98 | }) 99 | return res 100 | } else if (res.status === 404) { 101 | // && session?.user?.refreshToken) { 102 | console.log('Error: Needs refresh or unauthorized') 103 | vscode.postMessage({ 104 | command: 'error', 105 | text: 'This repository/branch was not found, or you do not have access to it. If this is your repo, please try signing in again. Reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 106 | }) 107 | setIsCloning(false) 108 | // todo: get refresh token 109 | } else { 110 | return res 111 | } 112 | }) 113 | } 114 | 115 | if (parsedRepo) { 116 | // if session user token exists, set repoUrl to include token before github.com and after https:// with user session token + '@' 117 | await submitJob().then(async (res) => { 118 | if (res.ok) { 119 | posthog.capture('Repository submitted', { 120 | source: 'greptile-vscode', 121 | repo: parsedRepo || '', 122 | }) 123 | if (!session?.state?.repos) { 124 | setSession({ 125 | ...session, 126 | state: { 127 | ...session?.state, 128 | chat: undefined, 129 | messages: [], 130 | repos: [parsedRepo], 131 | repoStates: { 132 | [parsedRepo]: { 133 | status: 'submitted', 134 | repository: parsedRepo, 135 | branch: '', 136 | remote: '', 137 | numFiles: 1, 138 | filesProcessed: 0, 139 | }, 140 | }, 141 | }, 142 | } as Session) 143 | } else { 144 | if (!session?.state?.repos.includes(parsedRepo)) { 145 | if (!session?.state?.chat) { 146 | setSession({ 147 | ...session, 148 | state: { 149 | ...session?.state, 150 | messages: [], 151 | repos: [...session?.state?.repos, parsedRepo], 152 | repoStates: { 153 | ...session?.state?.repoStates, 154 | [parsedRepo]: { 155 | status: 'submitted', 156 | repository: parsedRepo, 157 | branch: '', 158 | remote: '', 159 | numFiles: 1, 160 | filesProcessed: 0, 161 | }, 162 | }, 163 | }, 164 | } as Session) 165 | } else { 166 | setSession({ 167 | ...session, 168 | state: { 169 | ...session?.state, 170 | chat: { 171 | ...session?.state?.chat, 172 | repos: [...session?.state?.chat?.repos, parsedRepo], 173 | }, 174 | messages: [], 175 | repos: [...session?.state?.repos, parsedRepo], 176 | repoStates: { 177 | ...session?.state?.repoStates, 178 | [parsedRepo]: { 179 | status: 'submitted', 180 | repository: parsedRepo, 181 | branch: '', 182 | remote: '', 183 | numFiles: 1, 184 | filesProcessed: 0, 185 | }, 186 | }, 187 | }, 188 | } as Session) 189 | } 190 | } 191 | } 192 | } else { 193 | if (res.status === 401) { 194 | const message = await res.json().then((data) => data.response) 195 | vscode.postMessage({ 196 | command: 'error', 197 | text: `Permission error: ${message}`, 198 | }) 199 | console.log('Permission error: ', message) 200 | } else if (res.status === 404) { 201 | vscode.postMessage({ 202 | command: 'error', 203 | text: 'This repository/branch was not found, or you do not have access to it. If this is your repo, please try signing in again. Reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 204 | }) 205 | console.log('Repository not found') 206 | } else { 207 | vscode.postMessage({ 208 | command: 'error', 209 | text: `Unknown Error ${res.status} ${res.statusText}`, 210 | }) 211 | console.log(`Unknown Error: ${res.status} ${res.statusText}`) 212 | } 213 | } 214 | }) 215 | } else { 216 | console.log('Invalid GitHub URL') 217 | vscode.postMessage({ 218 | command: 'error', 219 | text: 'Please enter a valid GitHub repository URL, like https://github.com/greptileai/greptile-vscode.', 220 | }) 221 | } 222 | 223 | setIsCloning(false) 224 | } 225 | 226 | const handleKeyDown = (event: React.KeyboardEvent) => { 227 | if (event.key === 'Enter') { 228 | // && !session?.state?.error) { 229 | handleClone() 230 | } 231 | } 232 | 233 | const someValidRepos = () => { 234 | if (session?.state?.repoStates) 235 | Object.values(session.state.repoStates).some((repoState: RepositoryInfo) => { 236 | // console.log('repo state: ', repoState) 237 | return ( 238 | (repoState.status !== 'completed' && repoState.sha) || repoState.status === 'completed' 239 | ) 240 | }) 241 | 242 | return false 243 | } 244 | 245 | return ( 246 |
247 | {session?.user ? ( 248 |
249 |
250 |

Enter a Repo:

251 |
252 | { 258 | setSession({ 259 | ...session, 260 | state: { 261 | ...session?.state, 262 | repoUrl: event.currentTarget.value, 263 | }, 264 | } as Session) 265 | }} 266 | > 267 | Github URL 268 | 269 |
273 | { 280 | setSession({ 281 | ...session, 282 | state: { 283 | ...session?.state, 284 | branch: event.currentTarget.value, 285 | }, 286 | } as Session) 287 | }} 288 | > 289 | Branch 290 | 291 | 298 | {isCloning ? 'Loading...' : 'Add'} 299 | 300 |
301 |
302 |
303 |
304 | {someValidRepos && session?.state?.repoStates ? ( 305 | Object.keys(session?.state?.repoStates).map((repoKey) => ( 306 | <> 307 | 308 | { 310 | if (session?.state?.repos?.length === 1) { 311 | vscode.postMessage({ 312 | command: 'info', 313 | text: 'If you would like a clean reset, please use Greptile: Reset Session in the command palette.', 314 | }) 315 | } else { 316 | setSession({ 317 | ...session, 318 | state: { 319 | ...session?.state, 320 | chat: { 321 | ...session?.state?.chat, 322 | repos: session?.state?.chat?.repos?.filter((r) => r !== repoKey), 323 | }, 324 | repos: session?.state?.repos?.filter((r) => r !== repoKey), 325 | repoStates: Object.keys(session?.state?.repoStates) 326 | .filter((r) => r !== repoKey) 327 | .reduce((newRepoStates, r) => { 328 | newRepoStates[r] = session?.state?.repoStates[r] 329 | return newRepoStates 330 | }, {}), 331 | }, 332 | } as Session) 333 | } 334 | }} 335 | /> 336 | 337 | {session?.state?.repoStates[repoKey].status !== 'completed' && ( 338 | 339 | 340 | 341 | )} 342 | 343 | )) 344 | ) : ( 345 | null 346 | )} 347 |
348 |
349 | ) : ( 350 |
351 | { 353 | posthog.capture('Github Sign-in Clicked', { source: 'greptile-vscode' }) 354 | vscode.postMessage({ command: 'signIn', text: 'github' }) 355 | }} 356 | > 357 | {/*
*/} 358 | Sign In 359 |
360 |
361 | )} 362 |
363 | ) 364 | } 365 | -------------------------------------------------------------------------------- /webview-ui/src/components/repo/chat-repo-chip-actions.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react' 2 | 3 | interface RepoChipActionProps { 4 | deleteRepo: () => void 5 | } 6 | 7 | export const RepoChipActions = ({ deleteRepo }: RepoChipActionProps) => { 8 | return ( 9 | 15 | 16 | 17 | ); 18 | }; -------------------------------------------------------------------------------- /webview-ui/src/components/repo/chat-repo-chip.tsx: -------------------------------------------------------------------------------- 1 | // import { useContext } from 'react' 2 | 3 | // import { SessionContext } from '../../providers/session-provider' 4 | // import { deserializeRepoKey } from '../../lib/greptile-utils' 5 | // import { RepositoryInfo } from '../../types/chat' 6 | 7 | // interface RepoChipProps { 8 | // repoKey: string 9 | // children?: React.ReactNode 10 | // } 11 | 12 | // export const RepoChip = ({ repoKey: sRepoKey, children }: RepoChipProps) => { 13 | // const { session, setSession } = useContext(SessionContext) 14 | // const repoStates = session?.state?.repoStates 15 | 16 | // if (!repoStates || !repoStates[sRepoKey]) return 17 | // const repoKey = deserializeRepoKey(sRepoKey) 18 | 19 | // const getStatusColor = (status: RepositoryInfo['status'] | 'readonly') => { 20 | // switch (status) { 21 | // case 'completed': 22 | // return 'text-green' 23 | // case 'submitted': 24 | // case 'cloning': 25 | // case 'processing': 26 | // return 'text-yellow' 27 | // case 'failed': 28 | // return 'text-red' 29 | // case 'queued': 30 | // default: 31 | // return 'text-gray' 32 | // } 33 | // } 34 | 35 | // const chipState = repoStates[sRepoKey].status || 'readonly' 36 | 37 | // return ( 38 | //
39 | // {' '} 40 | //

41 | // {repoKey.repository} ({repoKey.branch}) 42 | //

43 | // {children} 44 | //
45 | // ) 46 | // } 47 | 48 | import { useContext } from 'react'; 49 | import { SessionContext } from '../../providers/session-provider'; 50 | import { deserializeRepoKey } from '../../lib/greptile-utils'; 51 | import { RepositoryInfo } from '../../types/chat'; 52 | 53 | interface RepoChipProps { 54 | repoKey: string; 55 | children?: React.ReactNode; 56 | } 57 | 58 | export const RepoChip = ({ repoKey: sRepoKey, children }: RepoChipProps) => { 59 | const { session, setSession } = useContext(SessionContext); 60 | const repoStates = session?.state?.repoStates; 61 | 62 | if (!repoStates || !repoStates[sRepoKey]) return null; 63 | 64 | const repoKey = deserializeRepoKey(sRepoKey); 65 | 66 | const getStatusColor = (status: RepositoryInfo['status'] | 'readonly') => { 67 | switch (status) { 68 | case 'completed': 69 | return 'green'; 70 | case 'submitted': 71 | case 'cloning': 72 | case 'processing': 73 | return 'orange'; 74 | case 'failed': 75 | return 'red'; 76 | case 'queued': 77 | default: 78 | return 'gray'; 79 | } 80 | }; 81 | 82 | const chipState = repoStates[sRepoKey].status || 'readonly'; 83 | 84 | return ( 85 |
99 | 108 | 109 | {repoKey.repository} ({repoKey.branch}) 110 | 111 | {children} 112 |
113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /webview-ui/src/components/repo/chat-status.tsx: -------------------------------------------------------------------------------- 1 | // import { useState, useEffect, useContext } from 'react' 2 | // import { VSCodeButton } from '@vscode/webview-ui-toolkit/react' 3 | 4 | // import { SessionContext } from '../../providers/session-provider' 5 | // import { useChatLoadingState } from '../../providers/chat-state-loading-provider' 6 | // import { useChatState } from '../../providers/chat-state-provider' 7 | // import { API_BASE } from '../../data/constants' 8 | // import { vscode } from '../../lib/vscode-utils' 9 | 10 | // interface ChatStatusProps { 11 | // repoKey: string 12 | // } 13 | 14 | // export const ChatStatus = ({ repoKey }: ChatStatusProps) => { 15 | // const { session, setSession } = useContext(SessionContext) 16 | 17 | // const [progress, setProgress] = useState(0) 18 | // const [isRetrying, setIsRetrying] = useState(false) 19 | // const { chatLoadingState, chatLoadingStateDispatch } = useChatLoadingState() 20 | // const { chatState, chatStateDispatch } = useChatState() 21 | 22 | // // const repoInfo = session?.state?.repoStates[repoKey] || { 23 | // // status: 'submitted', 24 | // // repository: repoKey, 25 | // // branch: '', 26 | // // remote: '', 27 | // // numFiles: 1, 28 | // // filesProcessed: 0, 29 | // // } 30 | // const repoInfo = { 31 | // status: "processing", 32 | // repository: "test-repo", 33 | // branch: "main", 34 | // remote: "github", 35 | // numFiles: 10, 36 | // filesProcessed: 5 37 | // } 38 | 39 | // useEffect(() => { 40 | // setProgress((100 * (repoInfo?.filesProcessed || 1)) / (repoInfo?.numFiles || 1)) 41 | // }, [repoInfo?.filesProcessed, repoInfo?.numFiles]) 42 | 43 | // const steps = ['submitted', 'cloning', 'processing', 'completed', 'failed', undefined] 44 | 45 | // useEffect(() => { 46 | // // increment by 1 every second 47 | // const interval = setInterval(() => { 48 | // setProgress((progress) => (progress >= 100 ? 100 : progress + 2 / (repoInfo?.numFiles || 1))) 49 | // }, 100) 50 | // return () => clearInterval(interval) 51 | // }, [repoInfo?.numFiles]) 52 | 53 | // const currentStep = repoInfo?.status ? steps.indexOf(repoInfo.status) : 0 54 | // return repoInfo?.status === 'completed' || 55 | // (repoInfo?.status === 'processing' && repoInfo?.numFiles === repoInfo?.filesProcessed) ? ( 56 | //
57 | // ) : ( 58 | //
59 | // {/* 60 | // Processing{' '} 61 | // 62 | // {repoInfo.repository} ({repoInfo.branch}) 63 | // 64 | // */} 65 | //
66 | //
67 | // 72 | //
73 | // {currentStep <= 0 ? 'Submitting repository...' : 'Repository submitted'} 74 | //
75 | //
76 | //
77 | // 82 | //
83 | // {currentStep <= 1 84 | // ? currentStep < 1 85 | // ? 'Clone repository for processing' 86 | // : 'Cloning repository...' 87 | // : 'Repository cloned'} 88 | //
89 | //
90 | //
91 | // 99 | //
100 | // {currentStep <= 2 101 | // ? currentStep < 2 102 | // ? 'Process repository' 103 | // : 'Processing repository...' 104 | // : 'Repository processed'} 105 | //
106 | //
107 | // {( 108 | // Math.min((repoInfo?.filesProcessed || 0) / (repoInfo?.numFiles || 1), 0.99) * 100 109 | // ).toFixed(0)} 110 | // % 111 | //
112 | //
113 | // {repoInfo.status !== 'failed' ? ( 114 | //
115 | // 116 | //
Complete
117 | //
118 | // ) : ( 119 | // <> 120 | //
121 | // 122 | //
Failed to process
123 | //
124 | //
125 | // { 128 | // console.log('Retrying repository submission') 129 | // setIsRetrying(true) 130 | // chatLoadingStateDispatch({ 131 | // action: 'set_loading_repo_states', 132 | // payload: { 133 | // ...chatState.repoStates, 134 | // [repoKey]: { 135 | // ...chatState.repoStates[repoKey], 136 | // status: 'submitted', 137 | // }, 138 | // }, 139 | // }) 140 | // chatStateDispatch({ 141 | // action: 'set_repo_states', 142 | // payload: { 143 | // ...chatState.repoStates, 144 | // [repoKey]: { 145 | // ...chatState.repoStates[repoKey], 146 | // status: 'submitted', 147 | // }, 148 | // }, 149 | // }) 150 | // fetch(`${API_BASE}/prod/v1/repositories`, { 151 | // method: 'POST', 152 | // body: JSON.stringify({ 153 | // remote: repoInfo?.remote, 154 | // repository: repoInfo?.repository, 155 | // branch: repoInfo?.branch, 156 | // }), 157 | // headers: { 158 | // 'Content-Type': 'application/json', 159 | // 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 160 | // }, 161 | // }).then(async (res) => { 162 | // setIsRetrying(false) 163 | // if (res.ok) { 164 | // return res 165 | // } else if (res.status === 404) { 166 | // console.log('Error: Needs refresh or unauthorized') 167 | // vscode.postMessage({ 168 | // command: 'error', 169 | // text: 'This repository/branch was not found, or you do not have access to it. If this is your repo, please try signing in again. Reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 170 | // }) 171 | // } else { 172 | // vscode.postMessage({ 173 | // command: 'error', 174 | // text: 'Please reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 175 | // }) 176 | // } 177 | // return res 178 | // }) 179 | // }} 180 | // > 181 | // {isRetrying ? 'Loading...' : 'Retry'} 182 | // 183 | //
184 | // 185 | // )} 186 | //
187 | //
188 | // ) 189 | // } 190 | 191 | // interface CircularProgressBarProps { 192 | // progress: number 193 | // size: number 194 | // completed?: boolean 195 | // } 196 | 197 | // const CircularProgressBar = ({ progress, size, completed }: CircularProgressBarProps) => { 198 | // const strokeWidth = 2 199 | // const radius = size / 2 - strokeWidth 200 | // const circumference = radius * 2 * Math.PI 201 | // const strokeDasharray = `${Math.round((progress / 100) * circumference)} ${Math.round( 202 | // circumference 203 | // )}` 204 | // // console.log( 205 | // // "progress", 206 | // // progress, 207 | // // "size", 208 | // // size, 209 | // // "strokeWidth", 210 | // // strokeWidth, 211 | // // "radius", 212 | // // radius, 213 | // // "circumference", 214 | // // circumference, 215 | // // "strokeDasharray", 216 | // // strokeDasharray, 217 | // // ); 218 | 219 | // if (completed) { 220 | // return ( 221 | //
222 | //
223 | //
224 | // ) 225 | // } 226 | 227 | // return ( 228 | // 229 | // 236 | // 245 | // 246 | // ) 247 | // } 248 | import { useState, useEffect, useContext } from 'react'; 249 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; 250 | 251 | import { SessionContext } from '../../providers/session-provider'; 252 | import { useChatLoadingState } from '../../providers/chat-state-loading-provider'; 253 | import { useChatState } from '../../providers/chat-state-provider'; 254 | import { API_BASE } from '../../data/constants'; 255 | import { vscode } from '../../lib/vscode-utils'; 256 | 257 | interface ChatStatusProps { 258 | repoKey: string; 259 | } 260 | 261 | export const ChatStatus = ({ repoKey }: ChatStatusProps) => { 262 | const { session, setSession } = useContext(SessionContext); 263 | const [progress, setProgress] = useState(0); 264 | const [isRetrying, setIsRetrying] = useState(false); 265 | const { chatLoadingState, chatLoadingStateDispatch } = useChatLoadingState(); 266 | const { chatState, chatStateDispatch } = useChatState(); 267 | 268 | const repoInfo = { 269 | status: 'processing', 270 | repository: 'test-repo', 271 | branch: 'main', 272 | remote: 'github', 273 | numFiles: 10, 274 | filesProcessed: 5, 275 | }; 276 | 277 | useEffect(() => { 278 | setProgress((100 * (repoInfo?.filesProcessed || 1)) / (repoInfo?.numFiles || 1)); 279 | }, [repoInfo?.filesProcessed, repoInfo?.numFiles]); 280 | 281 | const steps = ['submitted', 'cloning', 'processing', 'completed', 'failed', undefined]; 282 | 283 | useEffect(() => { 284 | const interval = setInterval(() => { 285 | setProgress((progress) => (progress >= 100 ? 100 : progress + 2 / (repoInfo?.numFiles || 1))); 286 | }, 100); 287 | return () => clearInterval(interval); 288 | }, [repoInfo?.numFiles]); 289 | 290 | const currentStep = repoInfo?.status ? steps.indexOf(repoInfo.status) : 0; 291 | 292 | return repoInfo?.status === 'completed' || 293 | (repoInfo?.status === 'processing' && repoInfo?.numFiles === repoInfo?.filesProcessed) ? ( 294 | null 295 | ) : ( 296 |
297 |
298 |
299 | 304 |
305 |
306 | {currentStep <= 0 ? 'Submitting repository...' : 'Repository submitted'} 307 |
308 |
309 |
310 |
311 | 316 |
317 |
318 | {currentStep <= 1 319 | ? currentStep < 1 320 | ? 'Clone repository for processing' 321 | : 'Cloning repository...' 322 | : 'Repository cloned'} 323 |
324 |
325 |
326 |
327 | 335 |
336 |
337 | {currentStep <= 2 338 | ? currentStep < 2 339 | ? 'Process repository' 340 | : 'Processing repository...' 341 | : 'Repository processed'} 342 |
343 |
344 | {(Math.min((repoInfo?.filesProcessed || 0) / (repoInfo?.numFiles || 1), 0.99) * 100).toFixed(0)}% 345 |
346 |
347 | {repoInfo.status !== 'failed' ? ( 348 |
349 |
350 | 351 |
352 |
353 | Complete 354 |
355 |
356 | ) : ( 357 | <> 358 |
359 |
360 | 361 |
362 |
363 | Failed to process 364 |
365 |
366 |
367 |
368 | { 371 | console.log('Retrying repository submission'); 372 | setIsRetrying(true); 373 | chatLoadingStateDispatch({ 374 | action: 'set_loading_repo_states', 375 | payload: { 376 | ...chatState.repoStates, 377 | [repoKey]: { 378 | ...chatState.repoStates[repoKey], 379 | status: 'submitted', 380 | }, 381 | }, 382 | }); 383 | chatStateDispatch({ 384 | action: 'set_repo_states', 385 | payload: { 386 | ...chatState.repoStates, 387 | [repoKey]: { 388 | ...chatState.repoStates[repoKey], 389 | status: 'submitted', 390 | }, 391 | }, 392 | }); 393 | fetch(`${API_BASE}/prod/v1/repositories`, { 394 | method: 'POST', 395 | body: JSON.stringify({ 396 | remote: repoInfo?.remote, 397 | repository: repoInfo?.repository, 398 | branch: repoInfo?.branch, 399 | }), 400 | headers: { 401 | 'Content-Type': 'application/json', 402 | Authorization: 'Bearer ' + session?.user?.tokens?.github.accessToken, 403 | }, 404 | }).then(async (res) => { 405 | setIsRetrying(false); 406 | if (res.ok) { 407 | return res; 408 | } else if (res.status === 404) { 409 | console.log('Error: Needs refresh or unauthorized'); 410 | vscode.postMessage({ 411 | command: 'error', 412 | text: 'This repository/branch was not found, or you do not have access to it. If this is your repo, please try signing in again. Reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 413 | }); 414 | } else { 415 | vscode.postMessage({ 416 | command: 'error', 417 | text: 'Please reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 418 | }); 419 | } 420 | return res; 421 | }); 422 | }} 423 | > 424 | {isRetrying ? 'Loading...' : 'Retry'} 425 | 426 |
427 |
428 | 429 | )} 430 |
431 | ); 432 | }; 433 | 434 | interface CircularProgressBarProps { 435 | progress: number; 436 | size: number; 437 | completed?: boolean; 438 | } 439 | 440 | const CircularProgressBar = ({ progress, size, completed }: CircularProgressBarProps) => { 441 | const strokeWidth = 2; 442 | const radius = size / 2 - strokeWidth; 443 | const circumference = radius * 2 * Math.PI; 444 | const strokeDasharray = completed ? `${Math.round(circumference)} ${Math.round(circumference)}`: `${Math.round((progress / 100) * circumference)} ${Math.round(circumference)}`; 445 | 446 | if (completed) { 447 | return ( 448 | 449 | 456 | 465 | 466 | ); 467 | } 468 | 469 | return ( 470 | 471 | 478 | 487 | 488 | ); 489 | }; -------------------------------------------------------------------------------- /webview-ui/src/components/ui/codeblock.tsx: -------------------------------------------------------------------------------- 1 | // Inspired by Chatbot-UI and modified to fit the needs of this project 2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx 3 | 4 | import { FC, memo } from 'react' 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 6 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' 7 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react' 8 | 9 | import { useCopyToClipboard } from '../../lib/hooks/use-copy-to-clipboard' 10 | 11 | interface Props { 12 | language: string 13 | value: string 14 | } 15 | 16 | interface languageMap { 17 | [key: string]: string | undefined 18 | } 19 | 20 | export const programmingLanguages: languageMap = { 21 | 'javascript': '.js', 22 | 'python': '.py', 23 | 'java': '.java', 24 | 'c': '.c', 25 | 'cpp': '.cpp', 26 | 'c++': '.cpp', 27 | 'c#': '.cs', 28 | 'ruby': '.rb', 29 | 'php': '.php', 30 | 'swift': '.swift', 31 | 'objective-c': '.m', 32 | 'kotlin': '.kt', 33 | 'typescript': '.ts', 34 | 'go': '.go', 35 | 'perl': '.pl', 36 | 'rust': '.rs', 37 | 'scala': '.scala', 38 | 'haskell': '.hs', 39 | 'lua': '.lua', 40 | 'shell': '.sh', 41 | 'sql': '.sql', 42 | 'html': '.html', 43 | 'css': '.css', 44 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component 45 | } 46 | 47 | export const generateRandomString = (length: number, lowercase = false) => { 48 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0 49 | let result = '' 50 | for (let i = 0; i < length; i++) { 51 | result += chars.charAt(Math.floor(Math.random() * chars.length)) 52 | } 53 | return lowercase ? result.toLowerCase() : result 54 | } 55 | 56 | const CodeBlock: FC = memo(({ language, value }) => { 57 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 58 | 59 | const onCopy = () => { 60 | if (isCopied) return 61 | copyToClipboard(value) 62 | } 63 | 64 | return ( 65 |
66 |
67 | {language} 68 | 69 | {isCopied ? ( 70 |
71 | ) : ( 72 |
73 | )} 74 | Copy code 75 |
76 |
77 | 98 | {value} 99 | 100 |
101 | ) 102 | }) 103 | CodeBlock.displayName = 'CodeBlock' 104 | 105 | export { CodeBlock } 106 | -------------------------------------------------------------------------------- /webview-ui/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /webview-ui/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as DialogPrimitive from '@radix-ui/react-dialog' 3 | import { Cross2Icon } from '@radix-ui/react-icons' 4 | 5 | const Dialog = DialogPrimitive.Root 6 | 7 | const DialogTrigger = DialogPrimitive.Trigger 8 | 9 | const DialogPortal = DialogPrimitive.Portal 10 | 11 | const DialogClose = DialogPrimitive.Close 12 | 13 | const DialogOverlay = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ ...props }, ref) => ) 17 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 18 | 19 | const DialogContent = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ children, ...props }, ref) => ( 23 | 24 | 25 | 26 | {children} 27 | 28 | 29 | Close 30 | 31 | 32 | 33 | )) 34 | DialogContent.displayName = DialogPrimitive.Content.displayName 35 | 36 | const DialogHeader = ({ ...props }: React.HTMLAttributes) =>
37 | DialogHeader.displayName = 'DialogHeader' 38 | 39 | const DialogFooter = ({ ...props }: React.HTMLAttributes) =>
40 | DialogFooter.displayName = 'DialogFooter' 41 | 42 | const DialogTitle = React.forwardRef< 43 | React.ElementRef, 44 | React.ComponentPropsWithoutRef 45 | >(({ ...props }, ref) => ) 46 | DialogTitle.displayName = DialogPrimitive.Title.displayName 47 | 48 | const DialogDescription = React.forwardRef< 49 | React.ElementRef, 50 | React.ComponentPropsWithoutRef 51 | >(({ ...props }, ref) => ) 52 | DialogDescription.displayName = DialogPrimitive.Description.displayName 53 | 54 | export { 55 | Dialog, 56 | DialogPortal, 57 | DialogOverlay, 58 | DialogTrigger, 59 | DialogClose, 60 | DialogContent, 61 | DialogHeader, 62 | DialogFooter, 63 | DialogTitle, 64 | DialogDescription, 65 | } 66 | -------------------------------------------------------------------------------- /webview-ui/src/data/constants.ts: -------------------------------------------------------------------------------- 1 | type ISampleRepo = { 2 | repo: string 3 | shortName: string 4 | displayName: string 5 | } 6 | 7 | export const SAMPLE_REPOS: ISampleRepo[] = [ 8 | { 9 | repo: 'Significant-Gravitas/Auto-GPT', 10 | shortName: 'autoGPT', 11 | displayName: '🤖 autoGPT', 12 | }, 13 | { 14 | repo: 'posthog/posthog', 15 | shortName: 'posthog', 16 | displayName: '🦔 Posthog', 17 | }, 18 | { 19 | repo: 'pallets/flask', 20 | shortName: 'flask', 21 | displayName: '🌐 flask', 22 | }, 23 | { 24 | repo: 'facebook/react', 25 | shortName: 'react', 26 | displayName: '⚛️ React JS', 27 | }, 28 | { 29 | repo: 'microsoft/vscode', 30 | shortName: 'vs-code', 31 | displayName: '👩‍💻 VS Code', 32 | }, 33 | { 34 | repo: 'hwchase17/langchain', 35 | shortName: 'langchain', 36 | displayName: '🦜 langchain', 37 | }, 38 | ] 39 | 40 | export const API_BASE = 'https://api.greptile.com/v1' 41 | -------------------------------------------------------------------------------- /webview-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | import { PostHogProvider } from 'posthog-js/react' 5 | 6 | const options = { 7 | api_host: 'https://us.posthog.com', 8 | } 9 | 10 | const container = document.getElementById('root') 11 | const root = createRoot(container!) 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /webview-ui/src/lib/actions.tsx: -------------------------------------------------------------------------------- 1 | import { encode } from 'js-base64' 2 | 3 | import { fetcher } from './greptile-utils' 4 | import { API_BASE } from '../data/constants' 5 | import { Chat, RepositoryInfo } from '../types/chat' 6 | import type { Session } from '../types/session' 7 | 8 | export async function getChat( 9 | session_id: string, 10 | user_id: string, 11 | session: Session 12 | ): Promise { 13 | // check authorization here 14 | // console.log('getting chat', session_id, user_id) 15 | try { 16 | const chat: any = await fetcher(`${API_BASE}/chats/${session_id}`, { 17 | method: 'GET', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 21 | }, 22 | }) 23 | // console.log('results of getChat: ', chat) 24 | if (!chat || (user_id && chat.user_id !== user_id)) { 25 | // throw new Error('Chat did not return anything or user_id does not match') 26 | return null 27 | } 28 | 29 | return chat 30 | } catch (error) { 31 | console.log('Error getting chat: ', error) 32 | return null 33 | } 34 | } 35 | 36 | export async function getNewChat(userId: string, repos: string[]): Promise { 37 | // need to wait until chat history has been fetched and set 38 | console.log('Getting new chat') 39 | 40 | const session_id = 41 | Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 42 | 43 | return { 44 | user_id: userId, 45 | repos: repos, 46 | session_id, 47 | chat_log: [], 48 | timestamp: Math.floor(Date.now() / 1000).toString(), 49 | title: repos[0].split(':').slice(-1)[0], 50 | newChat: true, 51 | } 52 | } 53 | 54 | export async function getRepo( 55 | repoKey: string, // remote:repository:branch 56 | session: Session 57 | ): Promise { 58 | // console.log(repoKey) 59 | try { 60 | const repoInfo: any = await fetcher( 61 | `${API_BASE}/repositories/batch?repositories=${encode(repoKey, true)}`, 62 | { 63 | method: 'GET', 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 67 | }, 68 | } 69 | ) 70 | // console.log('repoinfo: ', repoInfo) 71 | return repoInfo 72 | } catch (error) { 73 | // console.log(error) 74 | 75 | return new Promise((resolve) => { 76 | setTimeout(() => { 77 | resolve( 78 | fetcher(`${API_BASE}/repositories/batch?repositories=${encode(repoKey, true)}`, { 79 | method: 'GET', 80 | headers: { 81 | 'Content-Type': 'application/json', 82 | 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 83 | }, 84 | }) 85 | ) 86 | }, 1000) 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /webview-ui/src/lib/greptile-utils.ts: -------------------------------------------------------------------------------- 1 | import { Tiktoken } from 'js-tiktoken' 2 | import axios from 'axios' 3 | import { CreateChatCompletionRequestMessage } from 'openai/resources/chat' 4 | 5 | import { Message, Source, RepoKey, Chat, OldChat } from '../types/chat' 6 | import type { Session } from '../types/session' 7 | 8 | export const checkRepoAuthorization = async ( 9 | // todo: check and update 10 | repoKeyInput: string, // serialized repoKey 11 | session: Session | null 12 | ) => { 13 | // console.log('Checking repo auth: ', repoKeyInput) 14 | const repoKey = deserializeRepoKey(repoKeyInput) 15 | 16 | repoKey.branch = '' 17 | 18 | const statusCode = await getRemote( 19 | serializeRepoKey(repoKey), 20 | 'api', 21 | session?.user?.tokens?.[repoKey.remote]?.accessToken 22 | ) 23 | .then((res) => { 24 | // console.log("check repo auth res", res.status); 25 | // if (!res.ok) throw new Error("could not access repo"); 26 | return res.data 27 | }) 28 | .then((json) => { 29 | const visibility = json?.['visibility'] || 'public' 30 | // console.log('Checking auth visibility', visibility, 'membership', session?.user?.membership) 31 | if (visibility !== 'public' && session?.user?.membership !== 'pro') return 402 32 | const size = json?.['size'] ? json['size'] : json?.['statistics']?.['repository_size'] 33 | if (size > 10000 && session?.user?.membership !== 'pro') return 426 34 | return 200 35 | }) 36 | .catch((err) => { 37 | console.log(`Auth: ${err}`) 38 | return 404 39 | }) 40 | 41 | // console.log('check repo auth returning ', statusCode) 42 | return statusCode 43 | } 44 | 45 | export const getDefaultBranch = async (repoKey: string, session: Session | null) => { 46 | const repoKeyObj = deserializeRepoKey(repoKey) 47 | 48 | const result = getRemote(repoKey, 'api', session?.user?.tokens?.[repoKeyObj.remote]?.accessToken) 49 | .then((res) => res.data) 50 | .then((json) => json.default_branch) 51 | .catch((err) => { 52 | throw new Error(err) 53 | }) 54 | 55 | return result 56 | } 57 | 58 | export const getLatestCommit = async (repoKey: string, session: Session | null) => { 59 | const repoKeyObj = deserializeRepoKey(repoKey) 60 | 61 | // console.log('Getting latest commit') 62 | const result = await getRemote( 63 | repoKey, 64 | 'commit', 65 | session?.user?.tokens?.[repoKeyObj.remote]?.accessToken 66 | ) 67 | .then((res) => res.data.sha) // TODO make sure that this is the correct field for gitlab 68 | .catch(() => undefined) 69 | 70 | return result 71 | } 72 | 73 | /** 74 | * parses URL or identifier of a repo, returns the identifier BRANCH IS NOT GUARANTEED (Serialized RepoKey) 75 | * in format remote:branch:repository 76 | * @param input URL or identifier of a repo, return the identifier 77 | * @returns string | null (null means failed to parse) (Serialized RepoKey or null) 78 | */ 79 | export const parseIdentifier = (input: string): string | null => { 80 | if (!isDomain(input)) { 81 | const regex = /^(([^:]*):([^:]*):|[^:]*)([^:]*)$/ 82 | const match = input.match(regex) 83 | if (!match) return null 84 | const keys = input.split(':') 85 | if (keys.length === 1) 86 | return serializeRepoKey({ 87 | remote: 'github', 88 | repository: keys[0].toLowerCase(), 89 | branch: '', 90 | }) 91 | if (keys.length === 3) { 92 | let remote = keys[0], 93 | branch = keys[1], 94 | repository = keys[2] 95 | if (remote === 'azure' && repository.split('/').length === 2) { 96 | let repository_list = repository.split('/') 97 | repository_list.push(repository_list[1]) 98 | repository = repository_list.join('/') 99 | } 100 | return serializeRepoKey({ 101 | remote: remote?.toLowerCase(), 102 | repository: repository?.toLowerCase(), 103 | branch: branch?.toLowerCase(), 104 | }) 105 | } 106 | return null // only 2 entries may be ambiguous (1 might be as well...) 107 | } 108 | if (!input.startsWith('http')) input = 'https://' + input 109 | if (input.endsWith('.git')) input = input.slice(0, -4) 110 | try { 111 | const url = new URL(input) 112 | const remote = (() => { 113 | try { 114 | const services = ['github', 'gitlab', 'bitbucket', 'azure'] 115 | return services.find((service) => url.hostname.includes(service)) || null 116 | } catch (e) { 117 | return null 118 | } 119 | })() 120 | if (!remote) return null 121 | let repository, branch, regex, match 122 | switch (remote) { 123 | case 'github': 124 | regex = /([a-zA-Z0-9\._-]+\/[a-zA-Z0-9\._-]+)[\/tree\/]*([a-zA-Z0-0\._-]+)?/ 125 | match = url.pathname.match(regex) 126 | repository = match?.[1] 127 | branch = match?.[2] 128 | break 129 | case 'gitlab': 130 | regex = /([a-zA-Z0-9\._-]+\/[a-zA-Z0-9\._-]+)(?:\/\-)?(?:(?:\/tree\/)([a-zA-Z0-0\._-]+))?/ 131 | match = url.pathname.match(regex) 132 | repository = match?.[1] 133 | branch = match?.[2] 134 | break 135 | case 'azure': 136 | regex = /([a-zA-Z0-9\.\/_-]+)/ 137 | match = url.pathname.match(regex) 138 | repository = match?.[1].split('/').filter((x) => x !== '_git' && x !== '') || [] 139 | repository.push(repository?.slice(-1)[0]) 140 | repository = repository.slice(0, 3).join('/') 141 | branch = url.searchParams.get('version')?.slice(2) // remove 'GB' from the beginning 142 | break 143 | default: 144 | return url.hostname 145 | } 146 | if (!repository) return null 147 | return serializeRepoKey({ 148 | remote: remote?.toLowerCase(), 149 | repository: repository?.toLowerCase(), 150 | branch: (branch || '').toLowerCase(), 151 | }) 152 | } catch (e) { 153 | return null 154 | } 155 | } 156 | 157 | // bad helper for now, hopefully will lead to cleaner solution when we abstract away identifer 158 | export function isDomain(input: string): boolean { 159 | try { 160 | new URL(input) 161 | const regex = /^(([^:]*):([^:]*):|[^:]*)([^:]*)$/ 162 | const match = input.match(regex) 163 | if (match) return false 164 | return true 165 | } catch (e) { 166 | return false 167 | } 168 | } 169 | 170 | export async function fetcher(input: RequestInfo, init?: RequestInit): Promise { 171 | const res = await fetch(input, init) 172 | if (!res.ok) { 173 | const json = await res.json() 174 | if (json.error) { 175 | const error = new Error(json.error) as Error & { 176 | status: number 177 | } 178 | error.status = res.status 179 | throw error 180 | } else { 181 | throw new Error('An unexpected error occurred') 182 | } 183 | } 184 | return res.json() 185 | } 186 | 187 | export function formatDate(input: string | number | Date): string { 188 | const date = new Date(input) 189 | return date.toLocaleDateString('en-US', { 190 | month: 'long', 191 | day: 'numeric', 192 | year: 'numeric', 193 | }) 194 | } 195 | 196 | export function countTokensInMessages( 197 | messages: CreateChatCompletionRequestMessage[], 198 | encoder: Tiktoken 199 | ) { 200 | // export function countTokensInMessages(messages: ChatCompletionRequestMessage[]) { 201 | // just return the number of characters for now 202 | // Temporary soln until we can get the encoder working 203 | // return JSON.stringify(messages).length / 3; 204 | 205 | let counter = 0 206 | messages.forEach((message) => { 207 | counter += 4 208 | for (const key of Object.keys(message)) { 209 | if (key === 'name') counter -= 1 210 | } 211 | for (const value of Object.values(message)) { 212 | counter += encoder.encode(String(value)).length 213 | } 214 | }) 215 | counter += 2 216 | return counter 217 | } 218 | 219 | // export function cleanTextForGPT(text: string): string { 220 | // // return text.replaceAll("<|endoftext|>", "< | end of text | >"); 221 | // return text.replaceAll( 222 | // "<|endoftext|>", 223 | // "<\u200B|\u200Bend\u200Bof\u200Btext\u200B|\u200B>", 224 | // ); 225 | // } 226 | 227 | export function cleanMessage(message: Message): Message { 228 | const contentChunks: string[] = [] 229 | const sources: Source[] = [] 230 | let agentStatus = '' 231 | 232 | const segments = message.content.split('\n') 233 | for (const segment of segments) { 234 | let type = '' 235 | let message: any = '' 236 | try { 237 | const parsedSegment = JSON.parse(segment) 238 | type = parsedSegment.type 239 | message = parsedSegment.message 240 | } catch (e) { 241 | // Long ignore strings are sometimes not able to be parsed 242 | if (segment.startsWith('{"type":"ignore"')) { 243 | continue 244 | } 245 | 246 | // can't parse as JSON, so it's probably a string 247 | contentChunks.push(segment + '\n') 248 | continue 249 | } 250 | 251 | // At this point, we have a JSON message 252 | if (type === 'status') { 253 | agentStatus = message 254 | } else if (type === 'sources') { 255 | sources.push(message) 256 | } else if (type === 'message') { 257 | contentChunks.push(message) 258 | } 259 | } 260 | 261 | return { 262 | ...message, 263 | content: contentChunks.join(''), 264 | agentStatus: agentStatus.length > 0 ? agentStatus : undefined, 265 | sources: sources.length > 0 ? sources.flat() : message?.sources, 266 | } 267 | } 268 | 269 | export const getNewSessionId = () => { 270 | const session_id = 271 | Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 272 | return session_id 273 | } 274 | 275 | export function deserializeRepoKey(repoKey: string): RepoKey { 276 | if (!repoKey) return 277 | let [remote, repository, branch] = repoKey.split(':') 278 | if (remote !== 'github' && remote !== 'gitlab' && remote !== 'azure') { 279 | remote = 'github' 280 | } 281 | if (!branch && !repository) { 282 | // old method 283 | repository = remote 284 | remote = 'github' 285 | branch = '' // get default branch outside of helper function (don't want to make async) 286 | } 287 | return { remote, branch, repository } 288 | } 289 | 290 | export function serializeRepoKey(repoKey: RepoKey): string { 291 | const { remote, repository, branch } = repoKey 292 | return `${remote}:${repository}:${branch}` 293 | } 294 | 295 | export function getRepoKeysFromParams(params: { 296 | [key: string]: string | string[] | undefined 297 | }): string[] { 298 | let paramRepos: string[] = [] 299 | if (Array.isArray(params.repo)) { 300 | paramRepos = params.repo.map((x) => parseIdentifier(decodeURIComponent(x.trim())) || '') 301 | } else if (typeof params.repo === 'string') { 302 | const repoKey = parseIdentifier(decodeURIComponent(params.repo)) || '' 303 | paramRepos = [repoKey] 304 | } 305 | return paramRepos 306 | } 307 | 308 | interface CloneProps { 309 | repo: string 310 | token?: string 311 | } 312 | 313 | interface SourceProps { 314 | repo: string 315 | branch: string 316 | filepath: string 317 | lines?: string[] 318 | } 319 | 320 | interface ApiProps { 321 | repo: string 322 | branch?: string 323 | } 324 | 325 | /** 326 | * Return a url for an action for a repo 327 | * @param repoKey repo identifier 328 | * @param action action to be done 329 | * @returns 330 | */ 331 | export function getRepoUrlForAction( 332 | repoKey: RepoKey, 333 | action: string, 334 | args?: any | undefined 335 | ): string | undefined { 336 | const remote = repoKey.remote 337 | const branch = repoKey.branch 338 | const repository = repoKey.repository 339 | 340 | const remote_source_url: { [key: string]: any } = { 341 | github: { 342 | clone: ({ repo, token }: CloneProps) => `https://github.com/${repo}.git`, 343 | api: ({ repo, branch }: ApiProps) => 344 | `https://api.github.com/repos/${repo}` + (branch ? `/branches/${branch}` : ''), 345 | source: ({ repo, branch, filepath, lines }: SourceProps) => 346 | `https://github.com/${repo}/blob/${branch}/${filepath}${ 347 | lines ? `#L${lines[0]}-L${lines[1]}` : '' 348 | }`, 349 | commit: ({ repo, branch }: ApiProps) => 350 | `https://api.github.com/repos/${repo}/commits/${branch}`, 351 | }, 352 | gitlab: { 353 | clone: ({ repo, token }: CloneProps) => `https://gitlab.com/${repo}.git`, 354 | api: ({ repo, branch }: ApiProps) => 355 | `https://gitlab.com/api/v4/projects/${encodeURIComponent(repo)}` + 356 | (branch ? `/repository/branches/${encodeURIComponent(branch)}` : '') + 357 | '?statistics=true', 358 | source: ({ repo, branch, filepath, lines }: SourceProps) => 359 | `https://gitlab.com/${repo}/-/blob/${branch}/${filepath}${ 360 | lines ? `#L${lines[0]}-L${lines[1]}` : '' 361 | }`, 362 | commit: ({ repo, branch }: ApiProps) => 363 | `https://gitlab.com/api/v4/projects/${encodeURIComponent(repo)}/repository/commits/${ 364 | branch ? encodeURIComponent(branch) : '' 365 | }`, 366 | }, 367 | azure: { 368 | clone: ({ repo, token }: CloneProps) => 369 | `https://dev.azure.com/${repo.split('/').slice(0, 2).join('/')}/_git/${ 370 | repo.split('/').slice(-1)[0] 371 | }`, 372 | api: ({ repo, branch }: ApiProps) => 373 | `https://dev.azure.com/${repo.split('/').slice(0, 2).join('/')}/_apis/git/repositories/${ 374 | repo.split('/').slice(-1)[0] 375 | }/refs/heads/${branch ? branch : ''}`, 376 | source: ({ repo, branch, filepath, lines }: SourceProps) => 377 | `https://dev.azure.com/${repo.split('/').slice(0, 2).join('/')}/_git/${ 378 | repo.split('/').slice(-1)[0] 379 | }/blob/${branch}/${filepath}${lines ? `#L${lines[0]}-L${lines[1]}` : ''}`, 380 | commit: ({ repo, branch }: ApiProps) => 381 | `https://dev.azure.com/${repo.split('/').slice(0, 2).join('/')}/_apis/git/repositories/${ 382 | repo.split('/').slice(-1)[0] 383 | }/commits/${branch}`, 384 | }, 385 | } 386 | 387 | if (!remote_source_url[remote] || !remote_source_url[remote][action]) return undefined 388 | 389 | return remote_source_url[remote][action]({ 390 | repo: repository, 391 | branch, 392 | ...args, 393 | }) 394 | } 395 | 396 | export function convertOldChatInfo(chat: OldChat): Chat { 397 | const { 398 | user_id, 399 | repos: dynamoRepos, 400 | session_id, 401 | chat_log, 402 | timestamp, 403 | title, 404 | additional_repos, 405 | repo, 406 | } = chat 407 | const repos = (dynamoRepos || []).concat(additional_repos || []).concat(repo ? [repo] : []) 408 | const newChat: Chat = { 409 | user_id, 410 | repos, 411 | session_id, 412 | chat_log, 413 | timestamp, 414 | title, 415 | newChat: true, 416 | } 417 | return newChat 418 | } 419 | 420 | async function getRemote(repoKey: string, action: string, token: string) { 421 | const dRepoKey = deserializeRepoKey(repoKey) 422 | if (dRepoKey.remote === 'github' && dRepoKey.repository === 'github') 423 | return { data: '', status: 499 } 424 | 425 | const url = getRepoUrlForAction(dRepoKey, action) 426 | if (!url) return { data: '', error: 'No url found for action', status: 500 } 427 | 428 | try { 429 | const headers: any = { 430 | 'Content-Type': 'application/json', 431 | 'Authorization': `Bearer ${token}`, 432 | } 433 | 434 | const externalResponse = await axios.get(url, { headers }) 435 | const data = await externalResponse.data 436 | // console.log('data retrieved') 437 | return { data: data, status: 200 } 438 | } catch (error) { 439 | console.log('Error fetching external api') 440 | return { data: '', error: 'Error fetching external API', status: 500 } 441 | } 442 | } 443 | 444 | export async function parseRepoInput(session: Session) { 445 | const repoUrl = session?.state?.repoUrl 446 | 447 | let parsedRepo = undefined 448 | if (repoUrl) { 449 | const identifier = parseIdentifier(repoUrl) 450 | if (!identifier) { 451 | return null 452 | } 453 | 454 | let branch = '' 455 | if (session?.state?.branch) { 456 | branch = session.state.branch 457 | } else { 458 | branch = await getDefaultBranch(identifier, session) 459 | } 460 | 461 | parsedRepo = identifier + `${branch}` 462 | } 463 | 464 | return parsedRepo 465 | } 466 | -------------------------------------------------------------------------------- /webview-ui/src/lib/hooks/use-copy-to-clipboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface useCopyToClipboardProps { 4 | timeout?: number 5 | } 6 | 7 | export function useCopyToClipboard({ timeout = 2000 }: useCopyToClipboardProps) { 8 | const [isCopied, setIsCopied] = React.useState(false) 9 | 10 | const copyToClipboard = (value: string) => { 11 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) { 12 | return 13 | } 14 | 15 | if (!value) { 16 | return 17 | } 18 | 19 | navigator.clipboard.writeText(value).then(() => { 20 | setIsCopied(true) 21 | 22 | setTimeout(() => { 23 | setIsCopied(false) 24 | }, timeout) 25 | }) 26 | } 27 | 28 | return { isCopied, copyToClipboard } 29 | } 30 | -------------------------------------------------------------------------------- /webview-ui/src/lib/hooks/use-enter-submit.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, type RefObject } from 'react' 2 | 3 | export function useEnterSubmit(): { 4 | formRef: RefObject 5 | onKeyDown: (event: React.KeyboardEvent) => void 6 | } { 7 | const formRef = useRef(null) 8 | 9 | const handleKeyDown = (event: React.KeyboardEvent): void => { 10 | if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) { 11 | formRef.current?.requestSubmit() 12 | event.preventDefault() 13 | } 14 | } 15 | 16 | return { formRef, onKeyDown: handleKeyDown } 17 | } 18 | -------------------------------------------------------------------------------- /webview-ui/src/lib/vscode-utils.ts: -------------------------------------------------------------------------------- 1 | import type { WebviewApi } from 'vscode-webview' 2 | 3 | /** 4 | * A utility wrapper around the acquireVsCodeApi() function, which enables 5 | * message passing and state management between the webview and extension 6 | * contexts. 7 | * 8 | * This utility also enables webview code to be run in a web browser-based 9 | * dev server by using native web browser features that mock the functionality 10 | * enabled by acquireVsCodeApi. 11 | */ 12 | class VSCodeAPIWrapper { 13 | private readonly vsCodeApi: WebviewApi | undefined 14 | 15 | constructor() { 16 | // Check if the acquireVsCodeApi function exists in the current development 17 | // context (i.e. VS Code development window or web browser) 18 | if (typeof acquireVsCodeApi === 'function') { 19 | this.vsCodeApi = acquireVsCodeApi() 20 | } 21 | } 22 | 23 | /** 24 | * Post a message (i.e. send arbitrary data) to the owner of the webview. 25 | * 26 | * @remarks When running webview code inside a web browser, postMessage will instead 27 | * log the given message to the console. 28 | * 29 | * @param message Abitrary data (must be JSON serializable) to send to the extension context. 30 | */ 31 | public postMessage(message: unknown) { 32 | if (this.vsCodeApi) { 33 | this.vsCodeApi.postMessage(message) 34 | } else { 35 | console.log(message) 36 | } 37 | } 38 | 39 | /** 40 | * Get the persistent state stored for this webview. 41 | * 42 | * @remarks When running webview source code inside a web browser, getState will retrieve state 43 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 44 | * 45 | * @return The current state or `undefined` if no state has been set. 46 | */ 47 | public getState(): unknown | undefined { 48 | if (this.vsCodeApi) { 49 | return this.vsCodeApi.getState() 50 | } else { 51 | const state = localStorage.getItem('vscodeState') 52 | return state ? JSON.parse(state) : undefined 53 | } 54 | } 55 | 56 | /** 57 | * Set the persistent state stored for this webview. 58 | * 59 | * @remarks When running webview source code inside a web browser, setState will set the given 60 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 61 | * 62 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved 63 | * using {@link getState}. 64 | * 65 | * @return The new state. 66 | */ 67 | public setState(newState: T): T { 68 | if (this.vsCodeApi) { 69 | return this.vsCodeApi.setState(newState) 70 | } else { 71 | localStorage.setItem('vscodeState', JSON.stringify(newState)) 72 | return newState 73 | } 74 | } 75 | } 76 | 77 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi. 78 | export const vscode = new VSCodeAPIWrapper() 79 | -------------------------------------------------------------------------------- /webview-ui/src/pages/chat-page.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useContext, useState } from 'react' 2 | import { VSCodeProgressRing } from '@vscode/webview-ui-toolkit/react' 3 | 4 | import { Chat as ChatComponent } from '../components/chat/chat' 5 | import { getChat, getNewChat, getRepo } from '../lib/actions' 6 | import { SAMPLE_REPOS } from '../data/constants' 7 | import { 8 | checkRepoAuthorization, 9 | deserializeRepoKey, 10 | getDefaultBranch, 11 | serializeRepoKey, 12 | } from '../lib/greptile-utils' 13 | import { vscode } from '../lib/vscode-utils' 14 | import { ChatStateProvider } from '../providers/chat-state-provider' 15 | import { SessionContext } from '../providers/session-provider' 16 | import { Chat, RepositoryInfo, Message } from '../types/chat' 17 | import { Session } from '../types/session' 18 | 19 | import '../App.css' 20 | 21 | export interface ChatPageProps {} 22 | 23 | export default function ChatPage({}: ChatPageProps) { 24 | const { session, setSession } = useContext(SessionContext) 25 | 26 | const session_id = session?.state?.chat?.session_id 27 | const user_id = session?.user?.userId // session?.state?.chat?.user_id 28 | 29 | if (!session?.user) { 30 | return
Sign in with GitHub to get started.
31 | } 32 | 33 | if (!session?.state?.repos) { 34 | return ( 35 |
36 |

No repository submitted.

37 |

Note: To sync this chat with your repositories, you may need to reload this view.

38 |
39 | ) 40 | } 41 | 42 | const [repos, setRepos] = useState(session?.state?.repos) 43 | const [repoStates, setRepoStates] = useState<{ [repoKey: string]: RepositoryInfo }>( 44 | session?.state?.repoStates 45 | ) 46 | 47 | useEffect(() => { 48 | if (!repoStates) return 49 | // console.log('Trying to set session to', { 50 | // ...session, 51 | // state: { 52 | // ...session?.state, 53 | // repoStates: { ...repoStates }, 54 | // }, 55 | // } as Session) 56 | setSession({ 57 | ...session, 58 | state: { 59 | ...session?.state, 60 | repoStates: { ...repoStates }, 61 | }, 62 | } as Session) 63 | }, [repoStates]) // chatState.repoStates 64 | 65 | useEffect(() => { 66 | async function fetchInfo() { 67 | // console.log('Running fetchInfo for', session?.state?.repos, session) 68 | 69 | // **************** get chat info ******************* 70 | 71 | // console.log('session id: ', session_id) 72 | // console.log('user id: ', user_id) 73 | 74 | let chat: Chat | undefined = undefined 75 | 76 | if (!session?.state?.chat) { 77 | chat = await getNewChat(user_id, session?.state?.repos) 78 | 79 | setSession({ 80 | ...session, 81 | state: { 82 | ...session?.state, 83 | chat: chat, 84 | }, 85 | } as Session) 86 | } else { 87 | chat = await getChat( 88 | session?.state?.chat?.session_id, 89 | session?.state?.chat?.user_id, 90 | session 91 | ) 92 | 93 | if (!chat) { 94 | chat = await getNewChat(user_id, session?.state?.repos) 95 | } 96 | 97 | setSession({ 98 | ...session, 99 | state: { 100 | ...session?.state, 101 | chat: chat, 102 | }, 103 | } as Session) 104 | } 105 | 106 | // if (!chat) console.log('no chat found') 107 | 108 | // **************** get repo info ******************* 109 | 110 | const repoKeys: string[] = repos // session?.state?.repos 111 | // setRepos(repoKeys) 112 | 113 | // get empty branches and set them in new db 114 | const getRepoInfoAndPermission = repoKeys.map(async (repoKey: string) => { 115 | const dRepoKey = deserializeRepoKey(repoKey) 116 | if (!dRepoKey.remote) dRepoKey.remote = 'github' 117 | if (!dRepoKey.branch) dRepoKey.branch = await getDefaultBranch(repoKey, session) 118 | 119 | // replace significant-gravitas/auto-gpt with significant-gravitas/autogpt 120 | // hacky solution, works for now. Ideally get the canonical name from the remote 121 | if (dRepoKey.repository.toLowerCase() === 'significant-gravitas/auto-gpt') { 122 | dRepoKey.repository = 'significant-gravitas/autogpt' 123 | } 124 | 125 | const completeRepoKey = serializeRepoKey(dRepoKey) 126 | // console.log('complete repo key: ', completeRepoKey) 127 | const status = SAMPLE_REPOS.map((repo) => repo.repo).includes(dRepoKey.repository) 128 | ? 200 129 | : await checkRepoAuthorization(completeRepoKey, session) 130 | if (status !== 200 && status !== 426) { 131 | vscode.postMessage({ 132 | command: 'error', 133 | text: `You are unauthorized to access ${dRepoKey.repository} (${dRepoKey.branch}) or it does not exist. If you believe this is a mistake, please reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.`, 134 | }) 135 | throw new Error('Unauthorized or does not exist') 136 | } 137 | 138 | // console.log('verified permission') 139 | 140 | let repoInfos = await getRepo(completeRepoKey, session) // returns [failed, responses] 141 | .catch((e) => { 142 | console.error(e) 143 | }) 144 | if (!repoInfos) { 145 | // console.log('No repo info') 146 | return 147 | } 148 | return [completeRepoKey, repoInfos] as [ 149 | string, 150 | any // RepositoryInfo 151 | ] 152 | }) 153 | 154 | const repoInfoAndPermission = await Promise.allSettled(getRepoInfoAndPermission) 155 | 156 | let successes = 0 157 | repoInfoAndPermission.forEach((promise) => { 158 | // console.log('promise: ', promise) 159 | if (promise.status === 'fulfilled') { 160 | const [repoKey, repoInformation] = promise.value 161 | if (!repoKey) return 162 | 163 | // todo: better error handling 164 | // console.log('repoInformation: ', repoInformation) 165 | 166 | if (repoInformation?.responses?.length > 0) { 167 | setRepoStates({ 168 | ...repoStates, 169 | [repoKey]: { 170 | ...repoInformation.responses[0], 171 | status: repoInformation.responses[0].status || 'submitted', 172 | }, 173 | }) 174 | 175 | successes++ 176 | } 177 | } else { 178 | console.error(promise.reason) 179 | } 180 | }) 181 | 182 | if (successes === 0) { 183 | vscode.postMessage({ 184 | command: 'error', 185 | text: 'There was an error processing your repo. Please try again or reach out to us on [Discord](https://discord.com/invite/xZhUcFKzu7) for support.', 186 | }) 187 | } 188 | } 189 | 190 | fetchInfo() 191 | }, []) 192 | 193 | if (!session?.state?.repoStates || !session?.state?.chat) { 194 | // console.log('session.state.repoStates: ', session?.state?.repoStates, 'session.state.chat: ', session?.state?.chat) 195 | return 196 | } 197 | 198 | const getRepositories = () => { 199 | if (repos.length === 1) return deserializeRepoKey(repos[0]).repository 200 | 201 | const repoNames: string = repos 202 | .reduce((completed, repo) => { 203 | const repoInfo = repoStates[repo] 204 | if ( 205 | repoInfo?.status === 'completed' || 206 | (repoInfo?.status === 'processing' && repoInfo?.numFiles === repoInfo?.filesProcessed) 207 | ) { 208 | completed.push(deserializeRepoKey(repo).repository) 209 | } 210 | return completed 211 | }, []) 212 | .join(', ') 213 | 214 | if (repoNames.length === 1) return repoNames[0] 215 | 216 | return ( 217 | repoNames.slice(0, repoNames.lastIndexOf(', ')) + 218 | ' and ' + 219 | repoNames.slice(repoNames.lastIndexOf(', ') + 2) 220 | ) 221 | } 222 | 223 | const firstMessage = { 224 | role: 'assistant', 225 | content: `Hi! I am an expert on the ${getRepositories()} repositor${ 226 | repos.length > 1 ? 'ies' : 'y' 227 | }.\ 228 | Ask me anything! To share your feedback with our team,\ 229 | click [here](https://calendly.com/dakshgupta/free-coffee).`, 230 | } as Message 231 | 232 | const formatted_chat_log = session?.state?.chat?.chat_log || [] 233 | 234 | return ( 235 |
236 | 242 | 247 | 248 |
249 | ) 250 | } 251 | -------------------------------------------------------------------------------- /webview-ui/src/providers/chat-state-loading-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useEffect, useContext, useReducer, useRef } from 'react' 2 | import { encode } from 'js-base64' 3 | 4 | import { useChatState } from './chat-state-provider' 5 | import { SessionContext } from './session-provider' 6 | import { API_BASE } from '../data/constants' 7 | import { fetcher, getLatestCommit, serializeRepoKey } from '../lib/greptile-utils' 8 | import { RepositoryInfo, RepoKey } from '../types/chat' 9 | import { Session } from '../types/session' 10 | 11 | export type ChatLoadingState = { 12 | loadingRepoStates: { [repo: string]: RepositoryInfo } 13 | } 14 | 15 | const initialChatState = { 16 | loadingRepoStates: {}, 17 | } 18 | 19 | export interface ChatStateAction { 20 | action: string 21 | payload: any 22 | } 23 | 24 | const chatLoadingStateReducer = (state: any, action: ChatStateAction) => { 25 | switch (action.action) { 26 | case 'set_loading_repo_states': 27 | return { 28 | ...state, 29 | loadingRepoStates: action.payload, 30 | } 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | const ChatLoadingStateContext = createContext<{ 37 | chatLoadingState: ChatLoadingState 38 | chatLoadingStateDispatch: React.Dispatch 39 | }>({ 40 | chatLoadingState: initialChatState, 41 | chatLoadingStateDispatch: (action: ChatStateAction) => {}, 42 | }) 43 | 44 | export const useChatLoadingState = () => { 45 | const context = useContext(ChatLoadingStateContext) 46 | if (context === undefined) { 47 | throw new Error('useChatLoadingState must be used within a ChatLoadingStateProvider') 48 | } 49 | return context 50 | } 51 | 52 | export function ChatLoadingStateProvider({ children }: { children: React.ReactNode }) { 53 | const { chatState, chatStateDispatch } = useChatState() 54 | const { session, setSession } = useContext(SessionContext) 55 | const [chatLoadingState, chatLoadingStateDispatch] = useReducer(chatLoadingStateReducer, { 56 | ...initialChatState, 57 | loadingRepoStates: session?.state?.repoStates, // chatState.repoStates, 58 | }) 59 | const isCancelled = useRef(false) 60 | useEffect(() => { 61 | // console.log('useEffect for polling', session?.state?.repoStates) 62 | isCancelled.current = false 63 | const poll = async () => { 64 | // console.log('polling repo states') 65 | let newRepoStates = session?.state?.repoStates 66 | 67 | const submitReposProcessing = Object.keys(newRepoStates).map(async (repoKey) => { 68 | if (!newRepoStates[repoKey]) return 69 | const version = await getLatestCommit(repoKey, session) // todo: ensure branch exists 70 | if ( 71 | newRepoStates[repoKey].sha && 72 | version !== newRepoStates[repoKey].sha && 73 | newRepoStates[repoKey].status === 'completed' 74 | ) { 75 | fetch(`${API_BASE}/prod/v1/repositories`, { 76 | method: 'POST', 77 | body: JSON.stringify({ 78 | remote: newRepoStates[repoKey].remote, 79 | repository: newRepoStates[repoKey].repository, 80 | branch: newRepoStates[repoKey].branch, 81 | notify: false, 82 | }), 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 86 | }, 87 | }) 88 | } 89 | }) 90 | await Promise.allSettled(submitReposProcessing) 91 | await new Promise((resolve) => setTimeout(resolve, 2000)) 92 | 93 | // set status to processing for all, to at least check once 94 | Object.keys(newRepoStates).forEach((repoKey) => { 95 | newRepoStates[repoKey] = { 96 | ...newRepoStates[repoKey], 97 | status: 'processing', 98 | } 99 | }) 100 | // console.log('newRepoStates1: ', newRepoStates) 101 | 102 | let maxTries = 1000000000 // lol 103 | const repoFailureCount: { [key: string]: number } = {} 104 | while ( 105 | !isCancelled.current && 106 | maxTries-- > 0 && 107 | Object.keys(newRepoStates).some((repo) => newRepoStates[repo]?.status !== 'completed') 108 | ) { 109 | // console.log('polling', newRepoStates, isCancelled.current) 110 | let repos = Object.keys(newRepoStates).filter( 111 | (repo) => 112 | !newRepoStates[repo]?.indexId || 113 | !newRepoStates[repo]?.repository || 114 | (newRepoStates[repo]?.status !== 'completed' && (repoFailureCount[repo] || 0) < 3) 115 | ) 116 | 117 | // potential problem: initialAdditionalRepos is deleted during chat 118 | // but once new repos are set it still polls for the repos as well. 119 | if (repos.length === 0) break 120 | const response: any = await fetcher( 121 | `${API_BASE}/repositories/batch?repositories=${repos 122 | .map((repo) => encode(repo, true)) 123 | .join(',')}`, 124 | { 125 | method: 'GET', 126 | headers: { 127 | 'Content-Type': 'application/json', 128 | 'Authorization': 'Bearer ' + session?.user?.tokens?.github.accessToken, 129 | }, 130 | } 131 | ).catch((e) => { 132 | console.log(e) 133 | }) 134 | 135 | for (const repoStatus of response.responses) { 136 | // TODO: handle error in some 137 | // console.log('repo status: ', repoStatus) 138 | 139 | const repoKey = serializeRepoKey({ 140 | remote: repoStatus.remote, 141 | repository: repoStatus.repository, 142 | branch: repoStatus.branch, 143 | } as RepoKey) 144 | newRepoStates[repoKey] = { 145 | ...newRepoStates[repoKey], 146 | ...repoStatus, 147 | numFiles: repoStatus.numFiles, // 148 | filesProcessed: repoStatus.filesProcessed, // 149 | status: repoStatus.status, // 150 | } 151 | if (repoStatus.status === 'failed') { 152 | repoFailureCount[repoKey] = (repoFailureCount[repoKey] || 0) + 1 153 | } 154 | } 155 | 156 | setSession({ 157 | ...session, 158 | state: { 159 | ...session?.state, 160 | repoStates: newRepoStates, 161 | }, 162 | } as Session) 163 | 164 | // console.log('polling set loading repos 1') 165 | chatLoadingStateDispatch({ 166 | action: 'set_loading_repo_states', 167 | payload: { ...chatLoadingState.loadingRepoStates, ...newRepoStates }, 168 | }) 169 | await new Promise((resolve) => setTimeout(resolve, 1000)) 170 | } 171 | // console.log('polling set loading repos 2') 172 | chatLoadingStateDispatch({ 173 | action: 'set_loading_repo_states', 174 | payload: { ...chatLoadingState.loadingRepoStates, ...newRepoStates }, 175 | }) 176 | 177 | // console.log('done polling, pushing changes to context above') 178 | // this update will trigger the poll again, but it should exit if done 179 | chatStateDispatch({ 180 | action: 'set_repo_states', 181 | payload: newRepoStates, 182 | }) 183 | } 184 | poll() 185 | return () => { 186 | // TODO: this is not exiting properly 187 | isCancelled.current = true 188 | } 189 | }, [session?.state?.repoStates, chatState.repoStates, chatStateDispatch]) 190 | 191 | return ( 192 | 193 | {children} 194 | 195 | ) 196 | } 197 | -------------------------------------------------------------------------------- /webview-ui/src/providers/chat-state-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useReducer } from 'react' 2 | 3 | import { type RepositoryInfo, ChatInfo } from '../types/chat' 4 | 5 | export type ChatState = { 6 | session_id: string 7 | repoStates: { [repo: string]: RepositoryInfo } 8 | disabled: { value: boolean; reason: string } 9 | chats: ChatInfo[] 10 | } 11 | 12 | const initialChatState = { 13 | disabled: { value: false, reason: '' }, 14 | repoStates: {}, 15 | session_id: '', 16 | chats: [], 17 | } 18 | 19 | export interface ChatStateAction { 20 | action: string 21 | payload: any 22 | } 23 | 24 | const chatStateReducer = (state: any, action: ChatStateAction) => { 25 | switch (action.action) { 26 | case 'set_chats': 27 | return { 28 | ...state, 29 | chats: action.payload, 30 | } 31 | case 'set_session_id': 32 | return { 33 | ...state, 34 | session_id: action.payload, 35 | } 36 | case 'set_disabled': 37 | return { 38 | ...state, 39 | disabled: action.payload, 40 | } 41 | case 'set_repo_states': 42 | return { 43 | ...state, 44 | repoStates: action.payload, 45 | } 46 | case 'set_streaming': 47 | return { 48 | ...state, 49 | streaming: action.payload, 50 | } 51 | default: 52 | return state 53 | } 54 | } 55 | 56 | const ChatStateContext = createContext<{ 57 | chatState: ChatState 58 | chatStateDispatch: React.Dispatch 59 | }>({ 60 | chatState: initialChatState, 61 | chatStateDispatch: (action: ChatStateAction) => {}, 62 | }) 63 | 64 | export const useChatState = () => { 65 | const context = useContext(ChatStateContext) 66 | if (context === undefined) { 67 | throw new Error('useChatState must be used within a ChatStateProvider') 68 | } 69 | return context 70 | } 71 | 72 | interface ChatStateProviderProps { 73 | sessionId: string 74 | repoStates: { [repo: string]: RepositoryInfo } 75 | } 76 | 77 | export function ChatStateProvider({ 78 | children, 79 | initialProvidedState, 80 | }: { 81 | children: React.ReactNode 82 | initialProvidedState: ChatStateProviderProps 83 | }) { 84 | const [chatState, chatStateDispatch] = useReducer(chatStateReducer, { 85 | ...initialChatState, 86 | ...initialProvidedState, 87 | }) 88 | 89 | return ( 90 | 91 | {children} 92 | 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /webview-ui/src/providers/session-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export const SessionContext = createContext(undefined) 4 | -------------------------------------------------------------------------------- /webview-ui/src/types/chat.d.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'ai' 2 | 3 | export type Source = { 4 | repository: string 5 | remote: string 6 | branch: string 7 | filepath: string 8 | linestart: number | null 9 | lineend: number | null 10 | summary: string 11 | } 12 | 13 | export type Message = { 14 | sources?: Source[] 15 | agentStatus?: string 16 | } & Message 17 | 18 | export type RepoKey = { 19 | repository: string 20 | remote: string 21 | branch: string 22 | } 23 | 24 | type OldChatInfo = { 25 | user_id: string 26 | repo: string 27 | additional_repos: string[] 28 | session_id: string 29 | timestamp: string 30 | title: string 31 | newChat: boolean // for new sessions 32 | repos?: string[] // encoded repokey list 33 | } 34 | 35 | export type ChatInfo = { 36 | user_id: string 37 | repos: string[] // encoded repokey list 38 | session_id: string 39 | timestamp: string 40 | title: string 41 | newChat: boolean // for new sessions 42 | } 43 | 44 | export type OldChat = OldChatInfo & { 45 | chat_log: Message[] 46 | parent_id?: string 47 | } 48 | 49 | export type Chat = ChatInfo & { 50 | chat_log: Message[] 51 | parent_id?: string 52 | } 53 | 54 | export type RepositoryInfo = RepoKey & { 55 | source_id: string 56 | indexId: string 57 | filesProcessed?: number 58 | numFiles?: number 59 | message?: string 60 | private: boolean 61 | sample_questions?: string[] 62 | sha?: string 63 | external?: boolean 64 | status?: 'completed' | 'failed' | 'cloning' | 'processing' | 'submitted' | 'queued' 65 | } 66 | 67 | type ServerActionResult = Promise< 68 | | Result 69 | | { 70 | error: string 71 | } 72 | > 73 | 74 | export type SharedChatsDatabaseEntry = { 75 | id: string 76 | repositories: string[] 77 | sharedWith: string[] 78 | private: boolean 79 | owner: string 80 | } 81 | -------------------------------------------------------------------------------- /webview-ui/src/types/session.d.ts: -------------------------------------------------------------------------------- 1 | import { Chat, Message, RepositoryInfo } from './chat' 2 | 3 | export enum Membership { 4 | Free = 'free', 5 | Pro = 'pro', 6 | Student = 'student', 7 | } 8 | 9 | export type Session = { 10 | state?: { 11 | url: string 12 | repos: string[] 13 | repoUrl: string // current value of url input 14 | branch: string // current value of branch input 15 | repoStates?: { [repo: string]: RepositoryInfo } 16 | chat?: Chat 17 | messages: Message[] 18 | isStreaming: boolean 19 | input: string 20 | } 21 | user: { 22 | userId?: string 23 | membership?: Membership | undefined 24 | checkoutSession?: string 25 | business?: boolean 26 | freeTrialDaysRemaining?: number 27 | 28 | /** Oauth access token */ 29 | tokens: ExternalTokens 30 | // the last OAuth provider used to sign in 31 | authProvider: string 32 | } 33 | } 34 | 35 | interface ExternalTokens { 36 | [key: string]: { 37 | accessToken: string 38 | idToken?: string 39 | refreshToken?: string 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webview-ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /webview-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "useDefineForClassFields": true, 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["./src", "**/*.tsx", "**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /webview-ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | manifest: true, 9 | outDir: 'build', 10 | rollupOptions: { 11 | input: './src/index.tsx', 12 | output: { 13 | entryFileNames: `assets/[name].js`, 14 | chunkFileNames: `assets/[name].js`, 15 | assetFileNames: `assets/[name].[ext]`, 16 | }, 17 | }, 18 | chunkSizeWarningLimit: 1600, 19 | }, 20 | // server: { 21 | // proxy: { 22 | // "/api": { 23 | // target: "http://localhost:3001/", 24 | // changeOrigin: true, 25 | // // rewrite: (path) => path.replace(/^\/api/, ''), 26 | // secure: false 27 | // } 28 | // } 29 | // } 30 | }) 31 | --------------------------------------------------------------------------------