├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── support.md └── workflows │ ├── ovsx-deploy.yml │ └── vscode-deploy.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .rubberduck ├── embedding │ └── .gitignore └── template │ └── drunken-pirate.rdt.md ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app └── vscode │ ├── .gitignore │ ├── asset │ ├── LICENSE.txt │ ├── README.md │ ├── media │ │ ├── drunken-pirate.gif │ │ ├── drunken-pirate.png │ │ ├── extension-icon.png │ │ ├── screenshot-code-explanation.png │ │ ├── screenshot-diagnose-errors.gif │ │ ├── screenshot-diagnose-errors.png │ │ ├── screenshot-edit-code-2.png │ │ ├── screenshot-edit-code.gif │ │ ├── screenshot-edit-code.png │ │ ├── screenshot-find-bugs.png │ │ ├── screenshot-generate-code.gif │ │ ├── screenshot-generate-test.gif │ │ ├── screenshot-start-chat.png │ │ └── sidebar-icon.svg │ ├── package.json │ └── walkthrough │ │ ├── chat.md │ │ ├── edit-code.md │ │ ├── generate-code.md │ │ ├── other-actions.md │ │ ├── project.md │ │ ├── rubberduck-templates.md │ │ ├── setup.md │ │ └── tips-and-tricks.md │ ├── bin │ └── package.sh │ ├── dev │ ├── extension │ │ └── dist │ ├── media │ ├── package.json │ ├── template │ ├── walkthrough │ └── webview │ │ ├── asset │ │ └── dist │ ├── package.json │ └── project.json ├── asset ├── rubberduck-header-2.gif ├── rubberduck-header-2.png ├── rubberduck-header.gif └── rubberduck-header.png ├── doc ├── architecture.md └── rubberduck-templates.md ├── lib ├── common │ ├── .gitignore │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ ├── util │ │ │ ├── index.ts │ │ │ └── nextId.ts │ │ └── webview-api │ │ │ ├── ConversationSchema.ts │ │ │ ├── ErrorSchema.ts │ │ │ ├── IncomingMessage.ts │ │ │ ├── OutgoingMessage.ts │ │ │ ├── PanelState.ts │ │ │ └── index.ts │ └── tsconfig.json ├── extension │ ├── .gitignore │ ├── package.json │ ├── project.json │ ├── src │ │ ├── ai │ │ │ ├── AIClient.ts │ │ │ └── ApiKeyManager.ts │ │ ├── chat │ │ │ ├── ChatController.ts │ │ │ ├── ChatModel.ts │ │ │ └── ChatPanel.ts │ │ ├── conversation │ │ │ ├── Conversation.ts │ │ │ ├── ConversationType.ts │ │ │ ├── ConversationTypesProvider.ts │ │ │ ├── DiffData.ts │ │ │ ├── Message.ts │ │ │ ├── input │ │ │ │ ├── getFilename.ts │ │ │ │ ├── getLanguage.ts │ │ │ │ ├── getOpenFiles.ts │ │ │ │ ├── getSelectedLocationText.ts │ │ │ │ ├── getSelectedRange.ts │ │ │ │ ├── getSelectedText.ts │ │ │ │ ├── getSelectionWithDiagnostics.ts │ │ │ │ ├── resolveVariable.ts │ │ │ │ ├── resolveVariables.ts │ │ │ │ └── validateVariable.ts │ │ │ ├── retrieval-augmentation │ │ │ │ ├── EmbeddingFile.ts │ │ │ │ ├── cosineSimilarity.ts │ │ │ │ └── executeRetrievalAugmentation.ts │ │ │ └── template │ │ │ │ ├── RubberduckTemplate.ts │ │ │ │ ├── RubberduckTemplateLoadResult.ts │ │ │ │ ├── loadRubberduckTemplateFromFile.ts │ │ │ │ ├── loadRubberduckTemplatesFromWorkspace.ts │ │ │ │ └── parseRubberduckTemplate.ts │ │ ├── diff │ │ │ ├── DiffEditor.ts │ │ │ └── DiffEditorManager.ts │ │ ├── extension.ts │ │ ├── index │ │ │ ├── chunk │ │ │ │ ├── Chunk.ts │ │ │ │ ├── calculateLinePositions.ts │ │ │ │ └── splitLinearLines.ts │ │ │ └── indexRepository.ts │ │ ├── logger.ts │ │ ├── vscode │ │ │ ├── getActiveEditor.ts │ │ │ └── readFileContent.ts │ │ └── webview │ │ │ ├── WebviewContainer.ts │ │ │ └── generateNonce.ts │ └── tsconfig.json └── webview │ ├── .eslintrc.json │ ├── .gitignore │ ├── asset │ ├── base.css │ ├── chat.css │ ├── codicon.ttf │ ├── codicons.css │ ├── diff-hardcoded-colors.css │ ├── diff-vscode-colors.css │ └── prism.js │ ├── package.json │ ├── project.json │ ├── src │ ├── component │ │ ├── ChatInput.tsx │ │ ├── CollapsedConversationView.tsx │ │ ├── ConversationHeader.tsx │ │ ├── DiffView.tsx │ │ ├── ErrorMessage.tsx │ │ ├── ExpandedConversationView.tsx │ │ ├── InstructionRefinementView.tsx │ │ └── MessageExchangeView.tsx │ ├── panel │ │ ├── ChatPanelView.tsx │ │ └── DiffPanelView.tsx │ ├── vscode │ │ ├── SendMessage.ts │ │ ├── StateManager.ts │ │ └── VsCodeApi.ts │ └── webview.tsx │ └── tsconfig.json ├── lint-staged.config.js ├── nx.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── project.json └── template ├── chat ├── chat-de.rdt.md ├── chat-en.rdt.md └── chat-fr.rdt.md ├── experimental └── find-code-rubberduck.rdt.md ├── fun ├── code-sonnet.rdt.md └── drunken-pirate.rdt.md └── task ├── diagnose-errors.rdt.md ├── document-code.rdt.md ├── edit-code.rdt.md ├── explain-code-w-context.rdt.md ├── explain-code.rdt.md ├── find-bugs.rdt.md ├── generate-code.rdt.md ├── generate-unit-test.rdt.md └── improve-readability.rdt.md /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "rubberduck-vscode", 3 | "projectOwner": "rubberduck-ai", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "none", 12 | "contributorsPerLine": 4, 13 | "contributorsSortAlphabetically": false, 14 | "skipCi": true, 15 | "badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)", 16 | "contributors": [ 17 | { 18 | "login": "lgrammel", 19 | "name": "Lars Grammel", 20 | "avatar_url": "https://avatars0.githubusercontent.com/u/205036?v=4", 21 | "profile": "http://larsgrammel.de", 22 | "contributions": [ 23 | "ideas", 24 | "code", 25 | "doc", 26 | "review", 27 | "question", 28 | "bug" 29 | ] 30 | }, 31 | { 32 | "login": "iainvm", 33 | "name": "Iain Majer", 34 | "avatar_url": "https://avatars.githubusercontent.com/u/2806167?v=4", 35 | "profile": "http://iainvm.github.io", 36 | "contributions": [ 37 | "bug", 38 | "code" 39 | ] 40 | }, 41 | { 42 | "login": "nicoespeon", 43 | "name": "Nicolas Carlo", 44 | "avatar_url": "https://avatars0.githubusercontent.com/u/1094774?v=4", 45 | "profile": "https://nicoespeon.com", 46 | "contributions": [ 47 | "code", 48 | "doc", 49 | "bug" 50 | ] 51 | }, 52 | { 53 | "login": "RatoGBM", 54 | "name": "RatoGBM", 55 | "avatar_url": "https://avatars.githubusercontent.com/u/80184495?v=4", 56 | "profile": "https://github.com/RatoGBM", 57 | "contributions": [ 58 | "bug" 59 | ] 60 | }, 61 | { 62 | "login": "lohnsonok", 63 | "name": "Lionel Okpeicha", 64 | "avatar_url": "https://avatars.githubusercontent.com/u/60504466?v=4", 65 | "profile": "https://www.lionelokpeicha.dev/", 66 | "contributions": [ 67 | "bug" 68 | ] 69 | }, 70 | { 71 | "login": "MercerK", 72 | "name": "MercerK", 73 | "avatar_url": "https://avatars.githubusercontent.com/u/13123338?v=4", 74 | "profile": "https://github.com/MercerK", 75 | "contributions": [ 76 | "bug" 77 | ] 78 | }, 79 | { 80 | "login": "lundeen-bryan", 81 | "name": "Lundeen.Bryan", 82 | "avatar_url": "https://avatars.githubusercontent.com/u/13512507?v=4", 83 | "profile": "https://github.com/lundeen-bryan", 84 | "contributions": [ 85 | "ideas" 86 | ] 87 | }, 88 | { 89 | "login": "DucoG", 90 | "name": "DucoG", 91 | "avatar_url": "https://avatars.githubusercontent.com/u/67788719?v=4", 92 | "profile": "https://github.com/DucoG", 93 | "contributions": [ 94 | "ideas" 95 | ] 96 | }, 97 | { 98 | "login": "sbstn87", 99 | "name": "sbstn87", 100 | "avatar_url": "https://avatars.githubusercontent.com/u/37237675?v=4", 101 | "profile": "https://github.com/sbstn87", 102 | "contributions": [ 103 | "ideas" 104 | ] 105 | }, 106 | { 107 | "login": "tennox", 108 | "name": "Manuel", 109 | "avatar_url": "https://avatars.githubusercontent.com/u/2084639?v=4", 110 | "profile": "https://dev.page/tennox", 111 | "contributions": [ 112 | "ideas" 113 | ] 114 | }, 115 | { 116 | "login": "alessandro-newzoo", 117 | "name": "alessandro-newzoo", 118 | "avatar_url": "https://avatars.githubusercontent.com/u/47320294?v=4", 119 | "profile": "https://github.com/alessandro-newzoo", 120 | "contributions": [ 121 | "ideas" 122 | ] 123 | }, 124 | { 125 | "login": "Void-n-Null", 126 | "name": "Void&Null", 127 | "avatar_url": "https://avatars.githubusercontent.com/u/70048414?v=4", 128 | "profile": "https://github.com/Void-n-Null", 129 | "contributions": [ 130 | "ideas" 131 | ] 132 | }, 133 | { 134 | "login": "WittyDingo", 135 | "name": "WittyDingo", 136 | "avatar_url": "https://avatars.githubusercontent.com/u/63050074?v=4", 137 | "profile": "https://github.com/WittyDingo", 138 | "contributions": [ 139 | "ideas" 140 | ] 141 | }, 142 | { 143 | "login": "eva-lam", 144 | "name": "Eva", 145 | "avatar_url": "https://avatars.githubusercontent.com/u/29745387?v=4", 146 | "profile": "https://github.com/eva-lam", 147 | "contributions": [ 148 | "ideas" 149 | ] 150 | }, 151 | { 152 | "login": "AlexeyLavrentev", 153 | "name": "AlexeyLavrentev", 154 | "avatar_url": "https://avatars.githubusercontent.com/u/105936590?v=4", 155 | "profile": "https://github.com/AlexeyLavrentev", 156 | "contributions": [ 157 | "ideas" 158 | ] 159 | }, 160 | { 161 | "login": "linshu123", 162 | "name": "linshu123", 163 | "avatar_url": "https://avatars.githubusercontent.com/u/2569559?v=4", 164 | "profile": "https://github.com/linshu123", 165 | "contributions": [ 166 | "doc" 167 | ] 168 | }, 169 | { 170 | "login": "unquietwiki", 171 | "name": "Michael Adams", 172 | "avatar_url": "https://avatars.githubusercontent.com/u/1007551?v=4", 173 | "profile": "https://unquietwiki.com", 174 | "contributions": [ 175 | "code", 176 | "bug" 177 | ] 178 | }, 179 | { 180 | "login": "restlessronin", 181 | "name": "restlessronin", 182 | "avatar_url": "https://avatars.githubusercontent.com/u/88921269?v=4", 183 | "profile": "https://github.com/restlessronin", 184 | "contributions": [ 185 | "code" 186 | ] 187 | }, 188 | { 189 | "login": "igor-kupczynski", 190 | "name": "Igor Kupczyński", 191 | "avatar_url": "https://avatars.githubusercontent.com/u/166651?v=4", 192 | "profile": "http://kupczynski.info/", 193 | "contributions": [ 194 | "code" 195 | ] 196 | } 197 | ], 198 | "commitType": "docs" 199 | } 200 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | prism.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier" 11 | ], 12 | "overrides": [], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint"], 19 | "rules": {} 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: When Rubberduck crashes or has undesired side-effects, it's a bug 4 | title: "" 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | ## Describe the bug 10 | 11 | _A clear and concise description of what the bug is._ 12 | 13 | ## How to reproduce 14 | 15 | _Please detail steps to reproduce the behavior. Add code if necessary._ 16 | 17 | ## Expected behavior 18 | 19 | _Clear and concise description of what you expected to happen._ 20 | 21 | ## Screenshots 22 | 23 | _If applicable, add screenshots to help explain your problem._ 24 | 25 | ## Additional information 26 | 27 | - Version of the extension impacted: _vX.X.X_ 28 | 29 | _🧙‍ Add any other context about the problem here._ 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | about: Suggest a new idea of what Rubberduck could do 4 | title: "" 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | ## Is this request related to a problem? Please describe. 10 | 11 | _A clear and concise description of what the problem is (e.g. "I'm always frustrated when […]")._ 12 | 13 | ## Describe the solution you'd like 14 | 15 | _A clear and concise description of what you want to happen._ 16 | 17 | ## Additional context 18 | 19 | _Add any other information or screenshots about the feature request here (e.g. a gif showing how another tool does it)._ 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Support 3 | about: You have a question or need some help 4 | title: "" 5 | labels: "question" 6 | assignees: "" 7 | --- 8 | 9 | _Tell us what you need help with._ 10 | -------------------------------------------------------------------------------- /.github/workflows/ovsx-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Open VSX Deploy 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 7 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Build & deploy to Open VSX 29 | run: pnpm deploy:ovsx -p ${{ secrets.OVSX_ACCESS_TOKEN }} 30 | env: 31 | NODE_OPTIONS: "--max_old_space_size=8192" 32 | -------------------------------------------------------------------------------- /.github/workflows/vscode-deploy.yml: -------------------------------------------------------------------------------- 1 | name: VSCode Deploy 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 7 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Build & deploy to VS Code 29 | run: pnpm deploy:vscode 30 | env: 31 | NODE_OPTIONS: "--max_old_space_size=8192" 32 | VSCE_PAT: ${{ secrets.VSCODE_ACCESS_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | prism.js -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: false 6 | }; 7 | -------------------------------------------------------------------------------- /.rubberduck/embedding/.gitignore: -------------------------------------------------------------------------------- 1 | *.json -------------------------------------------------------------------------------- /.rubberduck/template/drunken-pirate.rdt.md: -------------------------------------------------------------------------------- 1 | ../../template/fun/drunken-pirate.rdt.md -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "run - app/vscode", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}/app/vscode/dev"], 10 | "outFiles": ["${workspaceFolder}/app/vscode/dev/extension/dist/**/*.js"], 11 | "preLaunchTask": "build-extension" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "cSpell.words": [ 4 | "chatgpt", 5 | "codicon", 6 | "openai", 7 | "rubberduck", 8 | "templating", 9 | "walkthrough", 10 | "walkthroughs" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.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": "shell", 8 | "command": "pnpm build-extension", 9 | "group": "build", 10 | "problemMatcher": [], 11 | "label": "build-extension" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Rubberduck 2 | 3 | First of all, thank you for taking some of your time to contribute to the project. You're awesome 🦆👍 4 | 5 | ## Table of Contents 6 | 7 | - [Get started](#get-started) 8 | - [Run the tests](#run-the-tests) 9 | - [Run it locally](#run-it-locally) 10 | - [Open a PR and add acknowledge your contribution](#open-a-pr-and-add-acknowledge-your-contribution) 11 | - [Other Commands](#other-commands) 12 | 13 | ## Get started 14 | 15 | > Pre-requisite: you have installed [git][install-git], [node][install-node] and [pnpm][install-pnpm]. 16 | 17 | 1. Clone the repo: `git clone git@github.com:rubberduck-ai/rubberduck-vscode.git` 18 | 1. Go into the cloned repository: `cd rubberduck-vscode` 19 | 1. Install dependencies: `pnpm install` 20 | 1. Build the extension: `pnpm build-all` 21 | 22 | The project uses [TypeScript][typescript], [Vitest][vitest] for the tests and [Prettier][prettier] for the formatting. 23 | 24 | ## Run the tests 25 | 26 | You can run tests with `pnpm test` 27 | 28 | To run them in watch mode, use: `pnpm test-watch`. 29 | 30 | ## Run it locally 31 | 32 | You can use [VS Code's built-in debugger][vscode-debug-extension] on the project to try out your local extension. 33 | 34 | To build the project, press `F5`. It should run the `run - app/vscode` task. 35 | 36 | This will: 37 | 38 | 1. Build the project 39 | 2. Open a new "Extension Development Host" VS Code window, with your local code overriding your "Rubberduck" extension 40 | 41 | It's handy to test your changes in integration with VS Code API. 42 | 43 | ### Useful resources to start changing the code 44 | 45 | - [VS Code Extension API documentation][vscode-extension-docs] is a good start 46 | - [OpenAI API documentation][openai-docs] is also useful if you plan to change the prompts 47 | 48 | ### Code Style 49 | 50 | Style formatting is managed by [Prettier][prettier]. It runs as a pre-commit hook, so you shouldn't have to worry about it 👐 51 | 52 | ## Open a PR and add acknowledge your contribution 53 | 54 | You can open a Pull-Request at any time. It can even be a draft if you need to ask for guidance and help. Actually, we'd be pretty happy to assist you going in the best direction! 55 | 56 | Once everything is ready, open a Pull-Request (if it's not already done) and ask for a review. We'll do our best to review it asap. 57 | 58 | Finally, [use all-contributors bot command][all-contributors-bot-command] to add yourself to the list of contributors. It's very easy to do, you basically need to mention the bot in a comment of your PR. 59 | 60 | Whether it's code, design, typo or documentation, every contribution is welcomed! So again, thank you very, very much 🦆 61 | 62 | ## More documentation 63 | 64 | - You can find a brief introduction to the architecture of this extension [here][architecture-doc]. 65 | 66 | - You can find more info about adding AI Chat templates [here][template-doc]. 67 | 68 | ## Other Commands 69 | 70 | - **Lint**: `pnpm nx lint --skip-nx-cache` 71 | - **Package**: `pnpm nx run vscode:package`‍ 72 | 73 | 74 | 75 | [install-git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git 76 | [install-node]: https://nodejs.org/en/download/ 77 | [install-pnpm]: https://pnpm.io/installation 78 | [typescript]: https://www.typescriptlang.org/ 79 | [vitest]: https://vitest.dev/ 80 | [prettier]: https://prettier.io 81 | [vscode-extension-docs]: https://code.visualstudio.com/api 82 | [openai-docs]: https://platform.openai.com/docs/introduction 83 | [vscode-debug-extension]: https://code.visualstudio.com/api/get-started/your-first-extension#debugging-the-extension 84 | [all-contributors-bot-command]: https://allcontributors.org/docs/en/bot/usage#all-contributors-add 85 | [architecture-doc]: https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/architecture.md 86 | [template-doc]: https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/rubberduck-templates.md 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rubberduck 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 | -------------------------------------------------------------------------------- /app/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /app/vscode/asset/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rubberduck 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 | -------------------------------------------------------------------------------- /app/vscode/asset/README.md: -------------------------------------------------------------------------------- 1 | ![Rubberduck AI Chat](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/asset/rubberduck-header-2.gif) 2 | 3 | > #### AI chat in the Visual Studio Code side bar. Rubberduck can [generate code](#generate-code), [edit code](#edit-code), [explain code](#explain-code), [generate tests](#generate-tests), [find bugs](#find-bugs), [diagnose errors](#diagnose-errors), and more. You can even add [your own conversation templates](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/rubberduck-templates.md). 4 | 5 | # Setup 6 | 7 | ## OpenAI 8 | 9 | 1. Get an OpenAI API key from [platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys) (you'll need to sign up for an account) 10 | 2. Enter the API key with the `Rubberduck: Enter OpenAI API key` command 11 | 12 | ## Llama.cpp (experimental) 13 | 14 | You can use Rubberduck with local models, e.g. [CodeLlama-7B-Instruct](https://huggingface.co/TheBloke/CodeLlama-7B-Instruct-GGUF) running in [Llama.cpp](https://github.com/ggerganov/llama.cpp) (see [ModelFusion Llama.cpp setup](https://modelfusion.dev/integration/model-provider/llamacpp#setup)). To enable llama.cpp in Rubberduck, set the `Rubberduck: Model` setting to `llama.cpp`. 15 | 16 | ## Configuration Options 17 | 18 | - **rubberduck.syntaxHighlighting.useVisualStudioCodeColors**: Use the Visual Studio Code Theme colors for syntax highlighting in the diff viewer. Might not work with all themes. Default is `false`. 19 | 20 | # Features 21 | 22 | [AI Chat](#ai-chat) | [Generate Code](#generate-code) | [Edit Code](#edit-code) | [Explain Code](#explain-code) | [Generate Tests](#generate-tests) | [Find Bugs](#find-bugs) | [Diagnose Errors](#diagnose-errors) | [Custom Conversations](#custom-conversations) 23 | 24 | ## AI Chat 25 | 26 | Chat with Rubberduck about your code and software development topics. Rubberduck knows the editor selection at the time of conversation start. 27 | 28 | 1. You can start a chat using one of the following options: 29 | 1. Run the `Rubberduck: Start Chat 💬` command from the command palette. 30 | 1. Select the `Start Chat 💬` entry in the editor context menu (right-click, requires selection). 31 | 1. Use the "Start new chat" button in the side panel. 32 | 1. Use the keyboard shortcut: `Ctrl + Cmd + C` (Mac) or `Ctrl + Alt + C` (Windows / Linux). 33 | 1. Press 💬 on the MacOS touch bar (if available). 34 | 1. Ask a question in the new conversation thread in the Rubberduck sidebar panel. 35 | 36 | ![AI Chat](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-start-chat.png) 37 | 38 | # Generate Code 39 | 40 | Instruct Rubberduck to generate code for you. 41 | 42 | 1. You can start generating code using one of the following options: 43 | 1. Run the `Rubberduck: Generate Code 💬` command from the command palette. 44 | 1. Use the "Generate Code" toolbar button in the side panel. 45 | 1. Use the keyboard shortcut: `Ctrl + Cmd + G` (Mac) or `Ctrl + Alt + G` (Windows / Linux). 46 | 2. Describe what you want to generate in the new conversation thread in the Rubberduck sidebar panel. Rubberduck will generate code for you based on your description. Further messages can be used to refine the generated code. 47 | 48 | ![Generate Code](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-generate-code.gif) 49 | 50 | ## Edit Code 51 | 52 | Change the selected code by instructing Rubberduck to create an edit. 53 | 54 | 1. Select the code that you want to change in the editor. 55 | 1. Invoke the "Edit Code" command using one of the following options: 56 | 57 | 1. Run the `Rubberduck: Edit Code 💬` command from the command palette. 58 | 1. Select the `Edit Code 💬` entry in the editor context menu (right-click). 59 | 1. Use the keyboard shortcut: `Ctrl + Cmd + E` (Mac) or `Ctrl + Alt + E` (Windows / Linux). 60 | 61 | 1. Rubberduck will generate a diff view. 62 | 1. Provide additional instructions to Rubberduck in the chat thread. 63 | 1. When you are happy with the changes, apply them using the "Apply" button in the diff view. 64 | 65 | ![Edit Code](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-edit-code.gif) 66 | 67 | ## Explain Code 68 | 69 | Ask Rubberduck to explain the selected code. 70 | 71 | 1. Select the code that you want to have explained in the editor. 72 | 1. Invoke the "Explain Code" command using one of the following options: 73 | 1. Run the `Rubberduck: Explain Code 💬` command from the command palette. 74 | 1. Select the `Explain Code 💬` entry in the editor context menu (right-click). 75 | 1. The explanations shows up in the Rubberduck sidebar panel. 76 | 77 | ![Explain Code](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-code-explanation.png) 78 | 79 | ## Generate Unit Test 80 | 81 | Generate a unit test for the selected code. 82 | 83 | 1. Select a piece of code in the editor for which you want to generate a test case. 84 | 2. Invoke the "Generate Unit Test" command using one of the following options: 85 | 1. Run the `Rubberduck: Generate Unit Test 💬` command from the command palette. 86 | 1. Select the `Generate Unit Test 💬` entry in the editor context menu (right-click). 87 | 3. The test case shows up in a new editor tab. You can refine it in the conversation panel. 88 | 89 | ![Generate Test](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-generate-test.gif) 90 | 91 | ## Find Bugs 92 | 93 | Identify potential defects in your code. 94 | 95 | 1. Select a piece of code that you want to check for bugs. 96 | 2. Invoke the "Find Bugs" command using one of the following options: 97 | 1. Run the `Rubberduck: Find Bugs 💬` command from the command palette. 98 | 1. Select the `Find Bugs 💬` entry in the editor context menu (right-click). 99 | 3. Rubberduck will show you a list of potential bugs in the chat window. You can refine it in the conversation panel. 100 | 101 | ![Find Bugs](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-find-bugs.png) 102 | 103 | ## Diagnose Errors 104 | 105 | Let Rubberduck identify error causes and suggest fixes to fix compiler and linter errors faster. 106 | 107 | 1. Select a piece of code in the editor that contains errors. 108 | 2. Invoke the "Diagnose Errors" command using one of the following options: 109 | 1. Run the `Rubberduck: Diagnose Errors 💬` command from the command palette. 110 | 1. Select the `Diagnose Errors 💬` entry in the editor context menu (right-click). 111 | 3. A potential solution will be shown in the chat window. You can refine it in the conversation panel. 112 | 113 | ![Diagnose Errors](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-diagnose-errors.gif) 114 | 115 | # Custom Conversations 116 | 117 | What if you want to craft an AI Chat that knows _specifically_ about your conventions? 118 | How cool would it be to have the answers in your own language? 119 | 120 | You can craft your own conversation templates by adding `.rdt.md` files to the `.rubberduck/template` folder in your workspace. See the [Rubberduck Template docs](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/rubberduck-templates.md) for more information. 121 | 122 | To use custom conversations, run the "Rubberduck: Start Custom Chat… 💬" command. 123 | 124 | Here is an example of a [drunken pirate describing your code](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/template/fun/drunken-pirate.rdt.md): 125 | 126 | ![Describe code as a drunken pirate](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/drunken-pirate.gif) 127 | 128 | [Learn how to craft your own Rubberduck template](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/rubberduck-templates.md)! 129 | 130 | # Tips and Tricks 131 | 132 | Understanding these concepts will help you get the most out of Rubberduck. 133 | 134 | - **Be specific**. 135 | When you ask for, e.g., code changes, include concrete names and describe the desired outcome. Avoid vague references. 136 | - **Provide context**. 137 | You can include the programming language ("in Rust") or other relevant contexts for basic questions. 138 | You can select a meaningful code snippet for code explanations and error diagnosis. 139 | - **Do not trust answers blindly**. 140 | It's a big step for a rubber duck to be able to respond to your questions. 141 | It might respond with inaccurate answers, especially when talking about 142 | less well-known topics or when the conversation gets too detailed. 143 | - **Use different chat threads for different topics**. 144 | Shorter threads with specific topics will help Rubberduck respond more accurately. 145 | 146 | # Built With 147 | 148 | - [ModelFusion](https://modelfusion/dev) - AI library 149 | - [Prism.js](https://prismjs.com/) - Syntax highlighting 150 | - [React](https://reactjs.org/) - UI rendering 151 | -------------------------------------------------------------------------------- /app/vscode/asset/media/drunken-pirate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/drunken-pirate.gif -------------------------------------------------------------------------------- /app/vscode/asset/media/drunken-pirate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/drunken-pirate.png -------------------------------------------------------------------------------- /app/vscode/asset/media/extension-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/extension-icon.png -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-code-explanation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-code-explanation.png -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-diagnose-errors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-diagnose-errors.gif -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-diagnose-errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-diagnose-errors.png -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-edit-code-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-edit-code-2.png -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-edit-code.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-edit-code.gif -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-edit-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-edit-code.png -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-find-bugs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-find-bugs.png -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-generate-code.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-generate-code.gif -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-generate-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-generate-test.gif -------------------------------------------------------------------------------- /app/vscode/asset/media/screenshot-start-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/app/vscode/asset/media/screenshot-start-chat.png -------------------------------------------------------------------------------- /app/vscode/asset/media/sidebar-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/chat.md: -------------------------------------------------------------------------------- 1 | # AI Chat 2 | 3 | 1. You can start a chat using one of the following options: 4 | 1. Run the `Rubberduck: Start Chat 💬` command from the command palette. 5 | 1. Select the `Start Chat 💬` entry in the editor context menu (right-click, requires selection). 6 | 1. Use the "Start new chat" button in the side panel. 7 | 1. Use the keyboard shortcut: `Ctrl + Cmd + C` (Mac) or `Ctrl + Alt + C` (Windows / Linux). 8 | 1. Press 💬 on the MacOS touch bar (if available). 9 | 2. Ask a question in the new conversation thread in the Rubberduck sidebar panel. Rubberduck knows the editor selection at the time of conversation start. 10 | 11 | ![AI Chat](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-start-chat.png) 12 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/edit-code.md: -------------------------------------------------------------------------------- 1 | # Edit Code 2 | 3 | 1. Select the code that you want to change in the editor. 4 | 2. Invoke the "Edit Code" command using one of the following options: 5 | 1. Run the `Rubberduck: Edit Code 💬` command from the command palette. 6 | 1. Use the "Edit Code" toolbar button in the side panel. 7 | 1. Select the `Edit Code 💬` entry in the editor context menu (right-click). 8 | 1. Use the keyboard shortcut: `Ctrl + Cmd + E` (Mac) or `Ctrl + Alt + E` (Windows / Linux). 9 | 3. Rubberduck will generate a diff view. 10 | 4. Provide additional instructions to Rubberduck in the chat thread. 11 | 5. When you are happy with the changes, apply them using the "Apply" button in the diff view. 12 | 13 | ![Edit Code](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-edit-code.gif) 14 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/generate-code.md: -------------------------------------------------------------------------------- 1 | # Generate Code 2 | 3 | 1. You can start generating code using one of the following options: 4 | 1. Run the `Rubberduck: Generate Code 💬` command from the command palette. 5 | 1. Use the "Generate Code" toolbar button in the side panel. 6 | 1. Use the keyboard shortcut: `Ctrl + Cmd + G` (Mac) or `Ctrl + Alt + G` (Windows / Linux). 7 | 2. Describe what you want to generate in the new conversation thread in the Rubberduck sidebar panel. Rubberduck will generate code for you based on your description. Further messages can be used to refine the generated code. 8 | 9 | ![Generate Code](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-generate-code.gif) 10 | 11 | > 💡  You can create your own customized code generators with [Rubberduck Templates](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/rubberduck-templates.md). 12 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/other-actions.md: -------------------------------------------------------------------------------- 1 | # Other Actions 2 | 3 | Rubberduck supports many commands that can be accessed from the command palette, the custom chat command, and from the editor context menu: 4 | 5 | - 🪄 **Generate Unit Tests** _(requires editor selection)_ 6 | - 💬 **Explain Code** _(requires editor selection)_ 7 | - 🐛 **Finds Bugs** _(requires editor selection)_ 8 | - 🔎 **Diagnose Errors** _(requires editor selection with errors or warnings)_ 9 | - 📝 **Document Code** _(requires editor selection)_ 10 | - 👓 **Improve Readability** _(requires editor selection)_ 11 | 12 | ![Diagnose Errors](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/screenshot-diagnose-errors.gif) 13 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/project.md: -------------------------------------------------------------------------------- 1 | # Open Source Project 2 | 3 | Rubberduck for Visual Studio Code is an open source project. 4 | 5 | - [GitHub repository](https://github.com/rubberduck-ai/rubberduck-vscode) 6 | - [Discord](https://discord.gg/8KN2HmyZmn) 7 | 8 | ### Additional Extensions 9 | 10 | Additional functionality can be added to Rubberduck with the following extensions: 11 | 12 | - [Rubberduck React](https://marketplace.visualstudio.com/items?itemName=Rubberduck.rubberduck-react-vscode) 13 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/rubberduck-templates.md: -------------------------------------------------------------------------------- 1 | # Rubberduck Templates 2 | 3 | What if you want to craft an AI Chat that knows _specifically_ about your conventions? 4 | How cool would it be to have the answers in your own language? 5 | 6 | You can craft your own conversation templates by adding `.rdt.md` files to the `.rubberduck/template` folder in your workspace. See the [Rubberduck Template docs](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/rubberduck-templates.md) for more information. 7 | 8 | To use custom conversations, run the "Rubberduck: Start Custom Chat… 💬" command. 9 | 10 | Here is an example of a [drunken pirate describing your code](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/template/fun/drunken-pirate.rdt.md): 11 | 12 | ![Describe code as a drunken pirate](https://raw.githubusercontent.com/rubberduck-ai/rubberduck-vscode/main/app/vscode/asset/media/drunken-pirate.gif) 13 | 14 | [Learn how to craft your own Rubberduck Template](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/doc/rubberduck-templates.md)! 15 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/setup.md: -------------------------------------------------------------------------------- 1 | # Set Up Rubberduck with OpenAI 2 | 3 | Rubberduck uses the OpenAI API and requires an API key to work. You can get an API key from [platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys) (you'll need to sign up for an account). 4 | 5 | Once you have an API key, enter it with the `Rubberduck: Enter OpenAI API key` command. 6 | 7 | # Alternative: use local AI models with Llama.cpp (experimental) 8 | 9 | You can use Rubberduck with local models, e.g. [CodeLlama-7B-Instruct](https://huggingface.co/TheBloke/CodeLlama-7B-Instruct-GGUF) running in [Llama.cpp](https://github.com/ggerganov/llama.cpp) (see [ModelFusion Llama.cpp setup](https://modelfusion.dev/integration/model-provider/llamacpp#setup)). To enable llama.cpp in Rubberduck, set the `Rubberduck: Model` setting to `llama.cpp`. 10 | 11 | # Rubberduck Settings 12 | 13 | - **rubberduck.model**: Select the OpenAI model that you want to use. Supports GPT-3.5-Turbo and GPT-4. 14 | - **rubberduck.syntaxHighlighting.useVisualStudioCodeColors**: Use the Visual Studio Code Theme colors for syntax highlighting in the diff viewer. Might not work with all themes. Default is `false`. 15 | 16 | - **rubberduck.openAI.baseUrl**: Specify the URL to the OpenAI API. If you are using a proxy, you can set it here. 17 | - **rubberduck.logger.level**: Specify the verbosity of logs that will appear in 'Rubberduck: Show Logs'. 18 | 19 | - **rubberduck.action.startChat.showInEditorContextMenu**: Show the "Start chat" action in the editor context menu, when you right-click on the code. 20 | - **rubberduck.action.startCustomChat.showInEditorContextMenu**: Show the "Start custom chat" action in the editor context menu, when you right-click on the code. 21 | - **rubberduck.action.editCode.showInEditorContextMenu**: Show the "Edit Code action in the editor context menu, when you right-click on the code. 22 | - **rubberduck.action.explainCode.showInEditorContextMenu**: Show the "Explain code" action in the editor context menu, when you right-click on the code. 23 | - **rubberduck.action.findBugs.showInEditorContextMenu**: Show the "Find bugs" action in the editor context menu, when you right-click on the code. 24 | - **rubberduck.action.generateUnitTest.showInEditorContextMenu**: Show the "Generate unit test" in the editor context menu, when you right-click on the code. 25 | - **rubberduck.action.diagnoseErrors.showInEditorContextMenu**: Show the "Diagnose errors" in the editor context menu, when you right-click on the code. 26 | -------------------------------------------------------------------------------- /app/vscode/asset/walkthrough/tips-and-tricks.md: -------------------------------------------------------------------------------- 1 | # Tips and Tricks 2 | 3 | Understanding these concepts will help you get the most out of Rubberduck. 4 | 5 | - **Be specific**. 6 | When you ask for, e.g., code changes, include concrete names and describe the desired outcome. Avoid vague references. 7 | - **Provide context**. 8 | You can include the programming language ("in Rust") or other relevant contexts for basic questions. 9 | You can select a meaningful code snippet for code explanations and error diagnosis. 10 | - **Do not trust answers blindly**. 11 | It's a big step for a rubber duck to be able to respond to your questions. 12 | It might respond with inaccurate answers, especially when talking about 13 | less well-known topics or when the conversation gets too detailed. 14 | - **Use different chat threads for different topics**. 15 | Shorter threads with specific topics will help Rubberduck respond more accurately. 16 | -------------------------------------------------------------------------------- /app/vscode/bin/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Cleaning dist folder..." 4 | rm -rf dist 5 | 6 | echo "Copying assets..." 7 | cp -r asset dist 8 | 9 | echo "Copying CHANGELOG..." 10 | cp -r ../../CHANGELOG.md dist 11 | 12 | echo "Copying templates..." 13 | cp -r ../../template dist 14 | 15 | echo "Copying extension lib files..." 16 | mkdir -p dist/extension/dist 17 | cp dev/extension/dist/extension.js dist/extension/dist/extension.js 18 | 19 | echo "Copying webview lib files..." 20 | mkdir -p dist/webview/asset 21 | mkdir -p dist/webview/dist 22 | cp dev/webview/asset/* dist/webview/asset 23 | cp dev/webview/dist/webview.js dist/webview/dist/webview.js 24 | 25 | echo "Packaging extension..." 26 | cd dist 27 | pnpm vsce package --no-dependencies --no-rewrite-relative-links -------------------------------------------------------------------------------- /app/vscode/dev/extension/dist: -------------------------------------------------------------------------------- 1 | ../../../../lib/extension/dist -------------------------------------------------------------------------------- /app/vscode/dev/media: -------------------------------------------------------------------------------- 1 | ../asset/media -------------------------------------------------------------------------------- /app/vscode/dev/package.json: -------------------------------------------------------------------------------- 1 | ../asset/package.json -------------------------------------------------------------------------------- /app/vscode/dev/template: -------------------------------------------------------------------------------- 1 | ../../../template -------------------------------------------------------------------------------- /app/vscode/dev/walkthrough: -------------------------------------------------------------------------------- 1 | ../asset/walkthrough -------------------------------------------------------------------------------- /app/vscode/dev/webview/asset: -------------------------------------------------------------------------------- 1 | ../../../../lib/webview/asset -------------------------------------------------------------------------------- /app/vscode/dev/webview/dist: -------------------------------------------------------------------------------- 1 | ../../../../lib/webview/dist -------------------------------------------------------------------------------- /app/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rubberduck/vscode", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": {}, 6 | "devDependencies": {}, 7 | "dependencies": { 8 | "@rubberduck/extension": "*", 9 | "@rubberduck/webview": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/vscode/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "app/vscode", 3 | "targets": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "executor": "nx:noop" 7 | }, 8 | "package": { 9 | "dependsOn": ["build"], 10 | "executor": "nx:run-commands", 11 | "options": { 12 | "cwd": "app/vscode", 13 | "command": "bin/package.sh" 14 | } 15 | }, 16 | "publish-vscode": { 17 | "dependsOn": ["package"], 18 | "executor": "nx:run-commands", 19 | "options": { 20 | "cwd": "app/vscode", 21 | "command": "pnpm vsce publish -i `ls dist/*.vsix`" 22 | } 23 | }, 24 | "publish-ovsx": { 25 | "dependsOn": ["package"], 26 | "executor": "nx:run-commands", 27 | "options": { 28 | "cwd": "app/vscode", 29 | "command": "pnpm ovsx publish -i `ls dist/*.vsix`" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /asset/rubberduck-header-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/asset/rubberduck-header-2.gif -------------------------------------------------------------------------------- /asset/rubberduck-header-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/asset/rubberduck-header-2.png -------------------------------------------------------------------------------- /asset/rubberduck-header.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/asset/rubberduck-header.gif -------------------------------------------------------------------------------- /asset/rubberduck-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgrammel/rubberduck-vscode/e6ca6c1e99a20be44dd2da59bb5d7a85daacac9b/asset/rubberduck-header.png -------------------------------------------------------------------------------- /doc/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture - Rubberduck for Visual Studio Code 2 | 3 | ## Overview 4 | 5 | Rubberduck is a [Visual Studio Code extension](https://code.visualstudio.com/api). It has two main components: 6 | 7 | - The extension itself, which is the main entry point for the extension. It contains the extension static and logic. 8 | - The sidebar webview, which is a iframe that runs in the sidebar. It renders the UI and forwards user input to the extension. 9 | 10 | Visual Studio Code initializes the Rubberduck extension on load. The extension then sets up callback for e.g. the registered commands and initializes the internal structure. The webview is loaded by Visual Studio Code when it is first opened. 11 | 12 | ```mermaid 13 | graph TD 14 | subgraph Rubberduck Extension 15 | A[Rubberduck Extension] 16 | B[Rubberduck Side Bar Webview] 17 | end 18 | C[OpenAI API] 19 | D[Visual Studio Code] 20 | E[User] 21 | 22 | E -.-> B 23 | E -..-> D 24 | 25 | D --activation & callback--> A 26 | A --API calls-->D 27 | D --loads-->B 28 | 29 | A ==state==> B 30 | B ==messages==> A 31 | B --API calls--> D 32 | 33 | A --request completion----> C 34 | ``` 35 | 36 | ## Project Structure 37 | 38 | Rubberduck for Visual Studio Code is written in [TypeScript](https://www.typescriptlang.org/). It uses [pnpm](https://pnpm.io/) as package manager and [Nx](https://nx.dev/) for monorepo tooling. 39 | 40 | The project is structured as follows: 41 | 42 | - [`app/vscode`](https://github.com/rubberduck-ai/rubberduck-vscode/tree/main/app/vscode): Extension assets (e.g. icons, `package.json`, `README.md`, walkthrough pages) and packaging scripts. 43 | - [`doc`](https://github.com/rubberduck-ai/rubberduck-vscode/tree/main/doc): documentation (e.g. architecture) 44 | - [`lib/common`](https://github.com/rubberduck-ai/rubberduck-vscode/tree/main/lib/common): API definitions for the message and state protocol between the extension and the webview. Also contains shared types and utilities. 45 | - [`lib/extension`](https://github.com/rubberduck-ai/rubberduck-vscode/tree/main/lib/extension): The main extension logic. 46 | - [`lib/webview`](https://github.com/rubberduck-ai/rubberduck-vscode/tree/main/lib/webview): The webview. It is written using [React](https://reactjs.org/). 47 | - [`template`](https://github.com/rubberduck-ai/rubberduck-vscode/tree/main/template): Rubberduck Conversation Templates. Some are used in the extension, others are meant as examples for users. 48 | 49 | ## Extension Module: `lib/extension` 50 | 51 | The entrypoint for the extension is [`extension.ts`](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/lib/extension/src/extension.ts). It registers the commands and the webview panel. It also creates the chat model, panel and controller, which execute the main logic of the extension: 52 | 53 | - [`ChatModel.ts`](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/lib/extension/src/chat/ChatModel.ts): The chat model contains the different conversations and the currently active conversation. 54 | - [`ChatPanel.ts`](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/lib/extension/src/chat/ChatPanel.ts): The chat panel adds an abstraction layout over the webview panel o make it easier to use. 55 | - [`ChatController.ts`](https://github.com/rubberduck-ai/rubberduck-vscode/blob/main/lib/extension/src/chat/ChatController.ts): The chat controller handlers the different user actions, both from commands and from the webview. It executes logic, including chat creation, OpenAI API calls and updating the chat panel. 56 | -------------------------------------------------------------------------------- /lib/common/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist -------------------------------------------------------------------------------- /lib/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rubberduck/common", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "build/index.js", 6 | "devDependencies": {}, 7 | "dependencies": { 8 | "zod": "3.20.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/common/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "lib/common", 3 | "targets": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "executor": "nx:run-commands", 7 | "options": { 8 | "cwd": "lib/common", 9 | "command": "tsc" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as webviewApi from "./webview-api"; 2 | export * as util from "./util"; 3 | -------------------------------------------------------------------------------- /lib/common/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./nextId"; 2 | -------------------------------------------------------------------------------- /lib/common/src/util/nextId.ts: -------------------------------------------------------------------------------- 1 | export function createNextId({ prefix = "" }: { prefix: string }) { 2 | let id = 0; 3 | return () => `${prefix}${id++}`; 4 | } 5 | -------------------------------------------------------------------------------- /lib/common/src/webview-api/ConversationSchema.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { errorSchema } from "./ErrorSchema"; 3 | 4 | export const selectionSchema = zod.object({ 5 | filename: zod.string(), 6 | startLine: zod.number(), 7 | endLine: zod.number(), 8 | text: zod.string(), 9 | }); 10 | 11 | export type Selection = zod.infer; 12 | 13 | export const messageSchema = zod.object({ 14 | author: zod.union([zod.literal("user"), zod.literal("bot")]), 15 | content: zod.string(), 16 | }); 17 | 18 | export type Message = zod.infer; 19 | 20 | const messageExchangeContentSchema = zod.object({ 21 | type: zod.literal("messageExchange"), 22 | messages: zod.array(messageSchema), 23 | error: errorSchema.optional(), 24 | state: zod.discriminatedUnion("type", [ 25 | zod.object({ 26 | type: zod.literal("userCanReply"), 27 | responsePlaceholder: zod.union([zod.string(), zod.undefined()]), 28 | }), 29 | zod.object({ 30 | type: zod.literal("waitingForBotAnswer"), 31 | botAction: zod.union([zod.string(), zod.undefined()]), 32 | }), 33 | zod.object({ 34 | type: zod.literal("botAnswerStreaming"), 35 | partialAnswer: zod.string(), 36 | }), 37 | ]), 38 | }); 39 | 40 | export type MessageExchangeContent = zod.infer< 41 | typeof messageExchangeContentSchema 42 | >; 43 | 44 | const instructionRefinementContentSchema = zod.object({ 45 | type: zod.literal("instructionRefinement"), 46 | instruction: zod.string(), 47 | error: errorSchema.optional(), 48 | state: zod.discriminatedUnion("type", [ 49 | zod.object({ 50 | type: zod.literal("userCanRefineInstruction"), 51 | label: zod.union([zod.string(), zod.undefined()]), 52 | responseMessage: zod.union([zod.string(), zod.undefined()]), 53 | }), 54 | zod.object({ 55 | type: zod.literal("waitingForBotAnswer"), 56 | botAction: zod.union([zod.string(), zod.undefined()]), 57 | }), 58 | ]), 59 | }); 60 | 61 | export type InstructionRefinementContent = zod.infer< 62 | typeof instructionRefinementContentSchema 63 | >; 64 | 65 | export const conversationSchema = zod.object({ 66 | id: zod.string(), 67 | header: zod.object({ 68 | title: zod.string(), 69 | isTitleMessage: zod.boolean(), 70 | codicon: zod.string(), 71 | }), 72 | content: zod.discriminatedUnion("type", [ 73 | messageExchangeContentSchema, 74 | instructionRefinementContentSchema, 75 | ]), 76 | }); 77 | 78 | export type Conversation = zod.infer; 79 | -------------------------------------------------------------------------------- /lib/common/src/webview-api/ErrorSchema.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | export const errorSchema = zod.union([ 4 | zod.string(), 5 | zod.object({ 6 | title: zod.string(), 7 | message: zod.string(), 8 | level: zod 9 | .union([zod.literal("error"), zod.literal("warning")]) 10 | .default("error") 11 | .optional(), 12 | disableRetry: zod.boolean().optional(), 13 | disableDismiss: zod.boolean().optional(), 14 | }), 15 | ]); 16 | 17 | /** 18 | * Say what happened. 19 | * Provide re-assurance and explain why it happened. Suggest actions 20 | * to help them fix it and/or give them a way out. 21 | * 22 | * You can use Markdown syntax for `title` and `message`. 23 | * 24 | * @see https://wix-ux.com/when-life-gives-you-lemons-write-better-error-messages-46c5223e1a2f 25 | * 26 | * @example Simple scenario 27 | * "Unable to connect to OpenAI" 28 | * 29 | * @example More elaborate object 30 | * { 31 | * title: "Unable to connect to OpenAI", 32 | * message: "Your changes were saved, but we could not connect your account due to a technical issue on our end. Please try connecting again. If the issue keeps happening, [contact Support](#link-to-contact-support)." 33 | * } 34 | */ 35 | export type Error = zod.infer; 36 | -------------------------------------------------------------------------------- /lib/common/src/webview-api/IncomingMessage.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { panelStateSchema } from "./PanelState"; 3 | 4 | export const incomingMessageSchema = zod.object({ 5 | data: zod.object({ 6 | type: zod.literal("updateState"), 7 | state: panelStateSchema, 8 | }), 9 | }); 10 | 11 | /** 12 | * A message sent from the extension to the webview. 13 | */ 14 | export type IncomingMessage = zod.infer; 15 | -------------------------------------------------------------------------------- /lib/common/src/webview-api/OutgoingMessage.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { errorSchema } from "./ErrorSchema"; 3 | 4 | export const outgoingMessageSchema = zod.discriminatedUnion("type", [ 5 | zod.object({ 6 | type: zod.literal("startChat"), 7 | }), 8 | zod.object({ 9 | type: zod.literal("enterOpenAIApiKey"), 10 | }), 11 | zod.object({ 12 | type: zod.literal("clickCollapsedConversation"), 13 | data: zod.object({ 14 | id: zod.string(), 15 | }), 16 | }), 17 | zod.object({ 18 | type: zod.literal("deleteConversation"), 19 | data: zod.object({ 20 | id: zod.string(), 21 | }), 22 | }), 23 | zod.object({ 24 | type: zod.literal("exportConversation"), 25 | data: zod.object({ 26 | id: zod.string(), 27 | }), 28 | }), 29 | zod.object({ 30 | type: zod.literal("sendMessage"), 31 | data: zod.object({ 32 | id: zod.string(), 33 | message: zod.string(), 34 | }), 35 | }), 36 | zod.object({ 37 | type: zod.literal("reportError"), 38 | error: errorSchema, 39 | }), 40 | zod.object({ 41 | type: zod.literal("dismissError"), 42 | data: zod.object({ 43 | id: zod.string(), 44 | }), 45 | }), 46 | zod.object({ 47 | type: zod.literal("retry"), 48 | data: zod.object({ 49 | id: zod.string(), 50 | }), 51 | }), 52 | zod.object({ 53 | type: zod.literal("applyDiff"), 54 | }), 55 | zod.object({ 56 | type: zod.literal("insertPromptIntoEditor"), 57 | data: zod.object({ 58 | id: zod.string(), 59 | }), 60 | }), 61 | ]); 62 | 63 | /** 64 | * A message sent from the webview to the extension. 65 | */ 66 | export type OutgoingMessage = zod.infer; 67 | -------------------------------------------------------------------------------- /lib/common/src/webview-api/PanelState.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | import { conversationSchema } from "./ConversationSchema"; 3 | import { errorSchema } from "./ErrorSchema"; 4 | 5 | export const panelStateSchema = zod 6 | .discriminatedUnion("type", [ 7 | zod.object({ 8 | type: zod.literal("chat"), 9 | conversations: zod.array(conversationSchema), 10 | selectedConversationId: zod.union([zod.string(), zod.undefined()]), 11 | hasOpenAIApiKey: zod.boolean(), 12 | surfacePromptForOpenAIPlus: zod.boolean(), 13 | error: errorSchema.optional(), 14 | }), 15 | zod.object({ 16 | type: zod.literal("diff"), 17 | oldCode: zod.string(), 18 | newCode: zod.string(), 19 | languageId: zod.string().optional(), 20 | }), 21 | ]) 22 | .optional(); 23 | 24 | export type PanelState = zod.infer; 25 | -------------------------------------------------------------------------------- /lib/common/src/webview-api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ConversationSchema"; 2 | export * from "./ErrorSchema"; 3 | export * from "./IncomingMessage"; 4 | export * from "./OutgoingMessage"; 5 | export * from "./PanelState"; 6 | -------------------------------------------------------------------------------- /lib/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "target": "es2020", 7 | "lib": ["es2020"], 8 | "module": "commonjs", 9 | "types": ["node"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "rootDir": "./src", 14 | "outDir": "./build" 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.tsx"] 17 | } 18 | -------------------------------------------------------------------------------- /lib/extension/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist -------------------------------------------------------------------------------- /lib/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rubberduck/extension", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "vitest run src/", 7 | "test-watch": "vitest src/" 8 | }, 9 | "devDependencies": { 10 | "@types/handlebars": "4.1.0", 11 | "@types/marked": "4.0.8", 12 | "@types/vscode": "1.72.0" 13 | }, 14 | "dependencies": { 15 | "@rubberduck/common": "*", 16 | "handlebars": "4.7.8", 17 | "marked": "4.2.12", 18 | "modelfusion": "0.135.1", 19 | "secure-json-parse": "2.7.0", 20 | "simple-git": "3.21.0", 21 | "zod": "3.22.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/extension/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "lib/extension", 3 | "targets": { 4 | "compile": { 5 | "dependsOn": ["^build"], 6 | "executor": "nx:run-commands", 7 | "options": { 8 | "cwd": "lib/extension", 9 | "command": "tsc" 10 | } 11 | }, 12 | "build": { 13 | "dependsOn": ["compile"], 14 | "executor": "nx:run-commands", 15 | "options": { 16 | "cwd": "lib/extension", 17 | "command": "npx esbuild build/extension.js --external:vscode --bundle --platform=node --outfile=dist/extension.js" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/extension/src/ai/AIClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InstructionPrompt, 3 | OpenAITextEmbeddingResponse, 4 | TextStreamingModel, 5 | embed, 6 | llamacpp, 7 | openai, 8 | streamText, 9 | } from "modelfusion"; 10 | import * as vscode from "vscode"; 11 | import { z } from "zod"; 12 | import { Logger } from "../logger"; 13 | import { ApiKeyManager } from "./ApiKeyManager"; 14 | 15 | function getOpenAIBaseUrl(): string { 16 | return ( 17 | vscode.workspace 18 | .getConfiguration("rubberduck.openAI") 19 | .get("baseUrl", "https://api.openai.com/v1/") 20 | // Ensure that the base URL doesn't have a trailing slash: 21 | .replace(/\/$/, "") 22 | ); 23 | } 24 | 25 | function getModel() { 26 | return z 27 | .enum([ 28 | "gpt-4", 29 | "gpt-4-32k", 30 | "gpt-4-1106-preview", 31 | "gpt-4-0125-preview", 32 | "gpt-4-turbo-preview", 33 | "gpt-3.5-turbo", 34 | "gpt-3.5-turbo-16k", 35 | "gpt-3.5-turbo-1106", 36 | "gpt-3.5-turbo-0125", 37 | "llama.cpp", 38 | ]) 39 | .parse(vscode.workspace.getConfiguration("rubberduck").get("model")); 40 | } 41 | 42 | export class AIClient { 43 | private readonly apiKeyManager: ApiKeyManager; 44 | private readonly logger: Logger; 45 | 46 | constructor({ 47 | apiKeyManager, 48 | logger, 49 | }: { 50 | apiKeyManager: ApiKeyManager; 51 | logger: Logger; 52 | }) { 53 | this.apiKeyManager = apiKeyManager; 54 | this.logger = logger; 55 | } 56 | 57 | private async getOpenAIApiConfiguration() { 58 | const apiKey = await this.apiKeyManager.getOpenAIApiKey(); 59 | 60 | if (apiKey == undefined) { 61 | throw new Error( 62 | "No OpenAI API key found. " + 63 | "Please enter your OpenAI API key with the 'Rubberduck: Enter OpenAI API key' command." 64 | ); 65 | } 66 | 67 | return openai.Api({ 68 | baseUrl: getOpenAIBaseUrl(), 69 | apiKey, 70 | }); 71 | } 72 | 73 | async getTextStreamingModel({ 74 | maxTokens, 75 | stop, 76 | temperature = 0, 77 | }: { 78 | maxTokens: number; 79 | stop?: string[] | undefined; 80 | temperature?: number | undefined; 81 | }): Promise> { 82 | const modelConfiguration = getModel(); 83 | 84 | return modelConfiguration === "llama.cpp" 85 | ? llamacpp 86 | .CompletionTextGenerator({ 87 | // TODO the prompt format needs to be configurable for non-Llama2 models 88 | promptTemplate: llamacpp.prompt.Llama2, 89 | maxGenerationTokens: maxTokens, 90 | stopSequences: stop, 91 | temperature, 92 | }) 93 | .withInstructionPrompt() 94 | : openai 95 | .ChatTextGenerator({ 96 | api: await this.getOpenAIApiConfiguration(), 97 | model: modelConfiguration, 98 | maxGenerationTokens: maxTokens, 99 | stopSequences: stop, 100 | temperature, 101 | frequencyPenalty: 0, 102 | presencePenalty: 0, 103 | }) 104 | .withInstructionPrompt(); 105 | } 106 | 107 | async streamText({ 108 | prompt, 109 | maxTokens, 110 | stop, 111 | temperature = 0, 112 | }: { 113 | prompt: string; 114 | maxTokens: number; 115 | stop?: string[] | undefined; 116 | temperature?: number | undefined; 117 | }) { 118 | this.logger.log(["--- Start prompt ---", prompt, "--- End prompt ---"]); 119 | 120 | return streamText({ 121 | model: await this.getTextStreamingModel({ maxTokens, stop, temperature }), 122 | prompt: { instruction: prompt }, 123 | }); 124 | } 125 | 126 | async generateEmbedding({ input }: { input: string }) { 127 | try { 128 | const { embedding, rawResponse } = await embed({ 129 | model: openai.TextEmbedder({ 130 | api: await this.getOpenAIApiConfiguration(), 131 | model: "text-embedding-ada-002", 132 | }), 133 | value: input, 134 | fullResponse: true, 135 | }); 136 | 137 | return { 138 | type: "success" as const, 139 | embedding, 140 | totalTokenCount: (rawResponse as OpenAITextEmbeddingResponse).usage 141 | ?.total_tokens, 142 | }; 143 | } catch (error: any) { 144 | console.log(error); 145 | 146 | return { 147 | type: "error" as const, 148 | errorMessage: error?.message, 149 | }; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/extension/src/ai/ApiKeyManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const OPEN_AI_API_KEY_SECRET_KEY = "rubberduck.openAI.apiKey"; 4 | 5 | type UpdateEvent = "clear key" | "set key"; 6 | 7 | export class ApiKeyManager { 8 | private readonly secretStorage: vscode.SecretStorage; 9 | private messageEmitter = new vscode.EventEmitter(); 10 | private messageHandler: vscode.Disposable | undefined; 11 | 12 | constructor({ secretStorage }: { secretStorage: vscode.SecretStorage }) { 13 | this.secretStorage = secretStorage; 14 | } 15 | 16 | async clearOpenAIApiKey(): Promise { 17 | await this.secretStorage.delete(OPEN_AI_API_KEY_SECRET_KEY); 18 | this.messageEmitter.fire("clear key"); 19 | } 20 | 21 | async getOpenAIApiKey(): Promise { 22 | return this.secretStorage.get(OPEN_AI_API_KEY_SECRET_KEY); 23 | } 24 | 25 | async hasOpenAIApiKey(): Promise { 26 | const key = await this.getOpenAIApiKey(); 27 | return key !== undefined; 28 | } 29 | 30 | onUpdate: vscode.Event = (listener, thisArg, disposables) => { 31 | // We only want to execute the last listener to apply the latest change. 32 | this.messageHandler?.dispose(); 33 | this.messageHandler = this.messageEmitter.event( 34 | listener, 35 | thisArg, 36 | disposables 37 | ); 38 | return this.messageHandler; 39 | }; 40 | 41 | private async storeApiKey(apiKey: string): Promise { 42 | return this.secretStorage.store(OPEN_AI_API_KEY_SECRET_KEY, apiKey); 43 | } 44 | 45 | async enterOpenAIApiKey() { 46 | await this.clearOpenAIApiKey(); 47 | 48 | const apiKey = await vscode.window.showInputBox({ 49 | title: "Enter your Open AI API key", 50 | ignoreFocusOut: true, 51 | placeHolder: "Open AI API key", 52 | }); 53 | 54 | if (apiKey == null) { 55 | return; // user aborted input 56 | } 57 | 58 | await this.storeApiKey(apiKey); 59 | 60 | this.messageEmitter.fire("set key"); 61 | vscode.window.showInformationMessage("OpenAI API key stored."); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/extension/src/chat/ChatController.ts: -------------------------------------------------------------------------------- 1 | import { util, webviewApi } from "@rubberduck/common"; 2 | import * as vscode from "vscode"; 3 | import { AIClient } from "../ai/AIClient"; 4 | import { Conversation } from "../conversation/Conversation"; 5 | import { ConversationType } from "../conversation/ConversationType"; 6 | import { resolveVariables } from "../conversation/input/resolveVariables"; 7 | import { DiffEditorManager } from "../diff/DiffEditorManager"; 8 | import { ChatModel } from "./ChatModel"; 9 | import { ChatPanel } from "./ChatPanel"; 10 | 11 | export class ChatController { 12 | private readonly chatPanel: ChatPanel; 13 | private readonly chatModel: ChatModel; 14 | private readonly ai: AIClient; 15 | private readonly getConversationType: ( 16 | id: string 17 | ) => ConversationType | undefined; 18 | private readonly diffEditorManager: DiffEditorManager; 19 | private readonly basicChatTemplateId: string; 20 | private readonly generateConversationId: () => string; 21 | 22 | constructor({ 23 | chatPanel, 24 | chatModel, 25 | ai, 26 | getConversationType, 27 | diffEditorManager, 28 | basicChatTemplateId, 29 | }: { 30 | chatPanel: ChatPanel; 31 | chatModel: ChatModel; 32 | ai: AIClient; 33 | getConversationType: (id: string) => ConversationType | undefined; 34 | diffEditorManager: DiffEditorManager; 35 | basicChatTemplateId: string; 36 | }) { 37 | this.chatPanel = chatPanel; 38 | this.chatModel = chatModel; 39 | this.ai = ai; 40 | this.getConversationType = getConversationType; 41 | this.diffEditorManager = diffEditorManager; 42 | this.basicChatTemplateId = basicChatTemplateId; 43 | 44 | this.generateConversationId = util.createNextId({ 45 | prefix: "conversation-", 46 | }); 47 | } 48 | 49 | private async updateChatPanel() { 50 | await this.chatPanel.update(this.chatModel); 51 | } 52 | 53 | private async addAndShowConversation( 54 | conversation: T 55 | ): Promise { 56 | this.chatModel.addAndSelectConversation(conversation); 57 | 58 | await this.showChatPanel(); 59 | await this.updateChatPanel(); 60 | 61 | return conversation; 62 | } 63 | 64 | async showChatPanel() { 65 | await vscode.commands.executeCommand("rubberduck.chat.focus"); 66 | } 67 | 68 | async receivePanelMessage(rawMessage: unknown) { 69 | const message = webviewApi.outgoingMessageSchema.parse(rawMessage); 70 | const type = message.type; 71 | 72 | switch (type) { 73 | case "enterOpenAIApiKey": { 74 | await vscode.commands.executeCommand("rubberduck.enterOpenAIApiKey"); 75 | break; 76 | } 77 | case "clickCollapsedConversation": { 78 | this.chatModel.selectedConversationId = message.data.id; 79 | await this.updateChatPanel(); 80 | break; 81 | } 82 | case "sendMessage": { 83 | await this.chatModel 84 | .getConversationById(message.data.id) 85 | ?.answer(message.data.message); 86 | break; 87 | } 88 | case "startChat": { 89 | await this.createConversation(this.basicChatTemplateId); 90 | break; 91 | } 92 | case "deleteConversation": { 93 | this.chatModel.deleteConversation(message.data.id); 94 | await this.updateChatPanel(); 95 | break; 96 | } 97 | case "exportConversation": { 98 | await this.chatModel 99 | .getConversationById(message.data.id) 100 | ?.exportMarkdown(); 101 | break; 102 | } 103 | case "retry": { 104 | await this.chatModel.getConversationById(message.data.id)?.retry(); 105 | break; 106 | } 107 | case "dismissError": 108 | await this.chatModel 109 | .getConversationById(message.data.id) 110 | ?.dismissError(); 111 | break; 112 | case "insertPromptIntoEditor": 113 | await this.chatModel 114 | .getConversationById(message.data.id) 115 | ?.insertPromptIntoEditor(); 116 | break; 117 | case "applyDiff": 118 | case "reportError": { 119 | // Architecture debt: there are 2 views, but 1 outgoing message type 120 | // These are handled in the Conversation 121 | break; 122 | } 123 | default: { 124 | const exhaustiveCheck: never = type; 125 | throw new Error(`unsupported type: ${exhaustiveCheck}`); 126 | } 127 | } 128 | } 129 | 130 | async createConversation(conversationTypeId: string) { 131 | try { 132 | const conversationType = this.getConversationType(conversationTypeId); 133 | 134 | if (conversationType == undefined) { 135 | throw new Error(`No conversation type found for ${conversationTypeId}`); 136 | } 137 | 138 | const variableValues = await resolveVariables( 139 | conversationType.variables, 140 | { 141 | time: "conversation-start", 142 | } 143 | ); 144 | 145 | const result = await conversationType.createConversation({ 146 | conversationId: this.generateConversationId(), 147 | ai: this.ai, 148 | updateChatPanel: this.updateChatPanel.bind(this), 149 | diffEditorManager: this.diffEditorManager, 150 | initVariables: variableValues, 151 | }); 152 | 153 | if (result.type === "unavailable") { 154 | if (result.display === "info") { 155 | await vscode.window.showInformationMessage(result.message); 156 | } else if (result.display === "error") { 157 | await vscode.window.showErrorMessage(result.message); 158 | } else { 159 | await vscode.window.showErrorMessage("Required input unavailable"); 160 | } 161 | 162 | return; 163 | } 164 | 165 | await this.addAndShowConversation(result.conversation); 166 | 167 | if (result.shouldImmediatelyAnswer) { 168 | await result.conversation.answer(); 169 | } 170 | } catch (error: any) { 171 | console.log(error); 172 | await vscode.window.showErrorMessage(error?.message ?? error); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/extension/src/chat/ChatModel.ts: -------------------------------------------------------------------------------- 1 | import { Conversation } from "../conversation/Conversation"; 2 | 3 | export class ChatModel { 4 | conversations: Array = []; 5 | selectedConversationId: string | undefined; 6 | 7 | addAndSelectConversation(conversation: Conversation) { 8 | this.conversations.push(conversation); 9 | this.selectedConversationId = conversation.id; 10 | } 11 | 12 | getConversationById(id: string): Conversation | undefined { 13 | return this.conversations.find((conversation) => conversation.id === id); 14 | } 15 | 16 | deleteConversation(id: string) { 17 | this.conversations = this.conversations.filter( 18 | (conversation) => conversation.id !== id 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/extension/src/chat/ChatPanel.ts: -------------------------------------------------------------------------------- 1 | import { webviewApi } from "@rubberduck/common"; 2 | import * as vscode from "vscode"; 3 | import { ApiKeyManager } from "../ai/ApiKeyManager"; 4 | import { WebviewContainer } from "../webview/WebviewContainer"; 5 | import { ChatModel } from "./ChatModel"; 6 | 7 | function getConfigSurfacePromptForOpenAIPlus(): boolean { 8 | return vscode.workspace 9 | .getConfiguration("rubberduck.openAI") 10 | .get("surfacePromptForPlus", false); 11 | } 12 | 13 | export class ChatPanel implements vscode.WebviewViewProvider { 14 | public static readonly id = "rubberduck.chat"; 15 | 16 | private readonly disposables: vscode.Disposable[] = []; 17 | 18 | private messageEmitter = new vscode.EventEmitter(); 19 | 20 | readonly onDidReceiveMessage = this.messageEmitter.event; 21 | 22 | private readonly extensionUri: vscode.Uri; 23 | 24 | private webviewPanel: WebviewContainer | undefined; 25 | private apiKeyManager: ApiKeyManager; 26 | 27 | private state: webviewApi.PanelState; 28 | 29 | constructor({ 30 | extensionUri, 31 | apiKeyManager, 32 | hasOpenAIApiKey, 33 | }: { 34 | readonly extensionUri: vscode.Uri; 35 | apiKeyManager: ApiKeyManager; 36 | /** Needed since retrieving it is an async operation */ 37 | hasOpenAIApiKey: boolean; 38 | }) { 39 | this.extensionUri = extensionUri; 40 | this.apiKeyManager = apiKeyManager; 41 | 42 | const surfacePromptForOpenAIPlus = getConfigSurfacePromptForOpenAIPlus(); 43 | this.state = { 44 | type: "chat", 45 | selectedConversationId: undefined, 46 | conversations: [], 47 | hasOpenAIApiKey, 48 | surfacePromptForOpenAIPlus, 49 | }; 50 | 51 | this.apiKeyManager.onUpdate(async () => { 52 | if (this.state?.type !== "chat") { 53 | return; 54 | } 55 | 56 | const hasOpenAIApiKey = await this.apiKeyManager.hasOpenAIApiKey(); 57 | if (this.state.hasOpenAIApiKey === hasOpenAIApiKey) { 58 | return; 59 | } 60 | 61 | this.state.hasOpenAIApiKey = hasOpenAIApiKey; 62 | this.renderPanel(); 63 | }); 64 | } 65 | 66 | private async renderPanel() { 67 | return this.webviewPanel?.updateState(this.state); 68 | } 69 | 70 | async resolveWebviewView(webviewView: vscode.WebviewView) { 71 | this.webviewPanel = new WebviewContainer({ 72 | panelId: "chat", 73 | isStateReloadingEnabled: false, 74 | webview: webviewView.webview, 75 | extensionUri: this.extensionUri, 76 | }); 77 | 78 | const receiveMessageDisposable = this.webviewPanel.onDidReceiveMessage( 79 | (message: unknown) => { 80 | this.messageEmitter.fire(message); 81 | } 82 | ); 83 | 84 | this.disposables.push( 85 | webviewView.onDidDispose(() => { 86 | receiveMessageDisposable.dispose(); 87 | this.webviewPanel = undefined; 88 | }) 89 | ); 90 | 91 | this.disposables.push( 92 | webviewView.onDidChangeVisibility(async () => { 93 | if (webviewView.visible) { 94 | return this.renderPanel(); 95 | } 96 | }) 97 | ); 98 | 99 | // not using await here, to avoid having an infinite load-in-progress indicator 100 | this.renderPanel(); 101 | } 102 | 103 | async update(model: ChatModel) { 104 | const conversations: Array = []; 105 | for (const conversation of model.conversations) { 106 | conversations.push(await conversation.toWebviewConversation()); 107 | } 108 | 109 | const surfacePromptForOpenAIPlus = getConfigSurfacePromptForOpenAIPlus(); 110 | const hasOpenAIApiKey = await this.apiKeyManager.hasOpenAIApiKey(); 111 | this.state = { 112 | type: "chat", 113 | selectedConversationId: model.selectedConversationId, 114 | conversations, 115 | hasOpenAIApiKey, 116 | surfacePromptForOpenAIPlus, 117 | }; 118 | return this.renderPanel(); 119 | } 120 | 121 | dispose() { 122 | this.disposables.forEach((disposable) => disposable.dispose()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/ConversationType.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AIClient } from "../ai/AIClient"; 3 | import { DiffEditorManager } from "../diff/DiffEditorManager"; 4 | import { Conversation } from "./Conversation"; 5 | import { DiffData } from "./DiffData"; 6 | import { RubberduckTemplate } from "./template/RubberduckTemplate"; 7 | 8 | export type CreateConversationResult = 9 | | { 10 | type: "success"; 11 | conversation: Conversation; 12 | shouldImmediatelyAnswer: boolean; 13 | } 14 | | { 15 | type: "unavailable"; 16 | display?: undefined; 17 | } 18 | | { 19 | type: "unavailable"; 20 | display: "info" | "error"; 21 | message: string; 22 | }; 23 | 24 | export class ConversationType { 25 | readonly id: string; 26 | readonly label: string; 27 | readonly description: string; 28 | readonly source: "built-in" | "local-workspace" | "extension"; 29 | readonly variables: RubberduckTemplate["variables"]; 30 | 31 | private template: RubberduckTemplate; 32 | 33 | constructor({ 34 | template, 35 | source, 36 | }: { 37 | template: RubberduckTemplate; 38 | source: ConversationType["source"]; 39 | }) { 40 | this.template = template; 41 | 42 | this.id = template.id; 43 | this.label = template.label; 44 | this.description = template.description; 45 | this.source = source; 46 | this.variables = template.variables; 47 | } 48 | 49 | get tags(): RubberduckTemplate["tags"] { 50 | return this.template.tags; 51 | } 52 | 53 | async createConversation({ 54 | conversationId, 55 | ai, 56 | updateChatPanel, 57 | initVariables, 58 | diffEditorManager, 59 | }: { 60 | conversationId: string; 61 | ai: AIClient; 62 | updateChatPanel: () => Promise; 63 | initVariables: Record; 64 | diffEditorManager: DiffEditorManager; 65 | }): Promise { 66 | return { 67 | type: "success", 68 | conversation: new Conversation({ 69 | id: conversationId, 70 | initVariables, 71 | ai: ai, 72 | updateChatPanel, 73 | template: this.template, 74 | diffEditorManager, 75 | diffData: await this.getDiffData(), 76 | }), 77 | shouldImmediatelyAnswer: this.template.initialMessage != null, 78 | }; 79 | } 80 | 81 | hasDiffCompletionHandler(): boolean { 82 | const template = this.template; 83 | return ( 84 | template.initialMessage?.completionHandler?.type === 85 | "active-editor-diff" || 86 | template.response.completionHandler?.type === "active-editor-diff" 87 | ); 88 | } 89 | 90 | async getDiffData(): Promise { 91 | if (!this.hasDiffCompletionHandler()) { 92 | return undefined; 93 | } 94 | 95 | const activeEditor = vscode.window.activeTextEditor; 96 | 97 | if (activeEditor == null) { 98 | throw new Error("active editor required"); 99 | } 100 | 101 | const document = activeEditor.document; 102 | const range = activeEditor.selection; 103 | const selectedText = document.getText(range); 104 | 105 | if (selectedText.trim().length === 0) { 106 | throw new Error("no selection"); 107 | } 108 | 109 | const filename = document.fileName.split("/").pop(); 110 | 111 | if (filename == undefined) { 112 | throw new Error("no filename"); 113 | } 114 | 115 | return { 116 | filename, 117 | language: document.languageId, 118 | selectedText, 119 | range, 120 | editor: activeEditor, 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/ConversationTypesProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ConversationType } from "./ConversationType"; 3 | import { loadConversationFromFile } from "./template/loadRubberduckTemplateFromFile"; 4 | import { loadRubberduckTemplatesFromWorkspace } from "./template/loadRubberduckTemplatesFromWorkspace"; 5 | import { parseRubberduckTemplate } from "./template/parseRubberduckTemplate"; 6 | 7 | export class ConversationTypesProvider { 8 | private readonly extensionUri: vscode.Uri; 9 | private readonly extensionTemplates: string[] = []; 10 | private readonly conversationTypes = new Map(); 11 | 12 | constructor({ extensionUri }: { extensionUri: vscode.Uri }) { 13 | this.extensionUri = extensionUri; 14 | } 15 | 16 | getConversationType(id: string) { 17 | return this.conversationTypes.get(id); 18 | } 19 | 20 | getConversationTypes() { 21 | return [...this.conversationTypes.values()]; 22 | } 23 | 24 | registerExtensionTemplate({ template }: { template: string }) { 25 | this.extensionTemplates.push(template); 26 | } 27 | 28 | async loadConversationTypes() { 29 | this.conversationTypes.clear(); 30 | 31 | await this.loadBuiltInTemplates(); 32 | this.loadExtensionTemplates(); 33 | await this.loadWorkspaceTemplates(); 34 | } 35 | 36 | private async loadBuiltInTemplates() { 37 | const builtInConversationTypes = [ 38 | await this.loadBuiltinTemplate("chat", "chat-en.rdt.md"), 39 | await this.loadBuiltinTemplate("fun", "code-sonnet.rdt.md"), 40 | await this.loadBuiltinTemplate("fun", "drunken-pirate.rdt.md"), 41 | await this.loadBuiltinTemplate("task", "diagnose-errors.rdt.md"), 42 | await this.loadBuiltinTemplate("task", "document-code.rdt.md"), 43 | await this.loadBuiltinTemplate("task", "edit-code.rdt.md"), 44 | await this.loadBuiltinTemplate("task", "explain-code.rdt.md"), 45 | await this.loadBuiltinTemplate("task", "explain-code-w-context.rdt.md"), 46 | await this.loadBuiltinTemplate("task", "find-bugs.rdt.md"), 47 | await this.loadBuiltinTemplate("task", "generate-code.rdt.md"), 48 | await this.loadBuiltinTemplate("task", "generate-unit-test.rdt.md"), 49 | await this.loadBuiltinTemplate("task", "improve-readability.rdt.md"), 50 | ]; 51 | 52 | for (const conversationType of builtInConversationTypes) { 53 | this.conversationTypes.set(conversationType.id, conversationType); 54 | } 55 | } 56 | 57 | private async loadBuiltinTemplate(...path: string[]) { 58 | const fileUri = vscode.Uri.joinPath(this.extensionUri, "template", ...path); 59 | const result = await loadConversationFromFile(fileUri); 60 | 61 | if (result.type === "error") { 62 | throw new Error( 63 | `Failed to load chat template '${fileUri.toString()}': ${result.error}` 64 | ); 65 | } 66 | 67 | return new ConversationType({ 68 | template: result.template, 69 | source: "built-in", 70 | }); 71 | } 72 | 73 | private loadExtensionTemplates() { 74 | for (const templateText of this.extensionTemplates) { 75 | try { 76 | const result = parseRubberduckTemplate(templateText); 77 | 78 | if (result.type === "error") { 79 | vscode.window.showErrorMessage("Could not load extension template"); 80 | continue; 81 | } 82 | 83 | const template = result.template; 84 | this.conversationTypes.set( 85 | template.id, 86 | new ConversationType({ 87 | template, 88 | source: "extension", 89 | }) 90 | ); 91 | } catch (error) { 92 | vscode.window.showErrorMessage("Could not load extension template"); 93 | } 94 | } 95 | } 96 | 97 | private async loadWorkspaceTemplates() { 98 | const workspaceTemplateLoadingResults = 99 | await loadRubberduckTemplatesFromWorkspace(); 100 | for (const loadingResult of workspaceTemplateLoadingResults) { 101 | if (loadingResult.type === "error") { 102 | vscode.window.showErrorMessage( 103 | `Error loading conversation template from ${loadingResult.file.path}: ${loadingResult.error}` 104 | ); 105 | 106 | continue; 107 | } 108 | 109 | if (loadingResult.template.isEnabled === false) { 110 | continue; 111 | } 112 | 113 | const type = new ConversationType({ 114 | template: loadingResult.template, 115 | source: "local-workspace", 116 | }); 117 | this.conversationTypes.set(type.id, type); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/DiffData.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export type DiffData = { 4 | readonly filename: string; 5 | readonly range: vscode.Range; 6 | readonly selectedText: string; 7 | readonly language: string | undefined; 8 | readonly editor: vscode.TextEditor; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/Message.ts: -------------------------------------------------------------------------------- 1 | export type Message = { 2 | content: string; 3 | author: "user" | "bot"; 4 | }; 5 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/getFilename.ts: -------------------------------------------------------------------------------- 1 | import { getActiveEditor } from "../../vscode/getActiveEditor"; 2 | 3 | export const getFilename = async () => 4 | getActiveEditor()?.document?.fileName.split("/").pop(); 5 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/getLanguage.ts: -------------------------------------------------------------------------------- 1 | import { getActiveEditor } from "../../vscode/getActiveEditor"; 2 | 3 | export const getLanguage = async () => getActiveEditor()?.document?.languageId; 4 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/getOpenFiles.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getActiveEditor } from "../../vscode/getActiveEditor"; 3 | 4 | export const getOpenFiles = async () => { 5 | const contextDocuments = vscode.workspace.textDocuments.filter( 6 | (document) => document.uri.scheme === "file" 7 | ); 8 | 9 | return contextDocuments.map((document) => { 10 | return { 11 | name: document.fileName, 12 | language: document.languageId, 13 | content: document.getText(), 14 | }; 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/getSelectedLocationText.ts: -------------------------------------------------------------------------------- 1 | import { getActiveEditor } from "../../vscode/getActiveEditor"; 2 | import { getFilename } from "./getFilename"; 3 | 4 | export const getSelectedLocationText = async () => { 5 | const activeEditor = getActiveEditor(); 6 | 7 | if (activeEditor == undefined) { 8 | return undefined; 9 | } 10 | 11 | const selectedRange = activeEditor.selection; 12 | return `${await getFilename()} ${selectedRange.start.line + 1}:${ 13 | selectedRange.end.line + 1 14 | }`; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/getSelectedRange.ts: -------------------------------------------------------------------------------- 1 | import { getActiveEditor } from "../../vscode/getActiveEditor"; 2 | 3 | export const getSelectedRange = async () => getActiveEditor()?.selection; 4 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/getSelectedText.ts: -------------------------------------------------------------------------------- 1 | import { getActiveEditor } from "../../vscode/getActiveEditor"; 2 | 3 | export const getSelectedText = async () => { 4 | const activeEditor = getActiveEditor(); 5 | return activeEditor?.document?.getText(activeEditor?.selection); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/getSelectionWithDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getActiveEditor } from "../../vscode/getActiveEditor"; 3 | 4 | export const getSelectedTextWithDiagnostics = async ({ 5 | diagnosticSeverities, 6 | }: { 7 | diagnosticSeverities: Array<"error" | "warning" | "information" | "hint">; 8 | }): Promise => { 9 | const activeEditor = getActiveEditor(); 10 | 11 | if (activeEditor == undefined) { 12 | return undefined; 13 | } 14 | 15 | const { document, selection } = activeEditor; 16 | 17 | // expand range to beginning and end of line, because ranges tend to be inaccurate: 18 | const range = new vscode.Range( 19 | new vscode.Position(selection.start.line, 0), 20 | new vscode.Position(selection.end.line + 1, 0) 21 | ); 22 | 23 | const includedDiagnosticSeverities = diagnosticSeverities.map( 24 | (diagnostic) => { 25 | switch (diagnostic) { 26 | case "error": 27 | return vscode.DiagnosticSeverity.Error; 28 | case "warning": 29 | return vscode.DiagnosticSeverity.Warning; 30 | case "information": 31 | return vscode.DiagnosticSeverity.Information; 32 | case "hint": 33 | return vscode.DiagnosticSeverity.Hint; 34 | } 35 | } 36 | ); 37 | 38 | const diagnostics = vscode.languages 39 | .getDiagnostics(document.uri) 40 | .filter( 41 | (diagnostic) => 42 | includedDiagnosticSeverities.includes(diagnostic.severity) && 43 | // line based filtering, because the ranges tend to be to inaccurate: 44 | diagnostic.range.start.line >= range.start.line && 45 | diagnostic.range.end.line <= range.end.line 46 | ) 47 | .map((diagnostic) => ({ 48 | line: diagnostic.range.start.line, 49 | message: diagnostic.message, 50 | source: diagnostic.source, 51 | code: 52 | typeof diagnostic.code === "object" 53 | ? diagnostic.code.value 54 | : diagnostic.code, 55 | severity: diagnostic.severity, 56 | })); 57 | 58 | if (diagnostics.length === 0) { 59 | return undefined; 60 | } 61 | 62 | return annotateSelectionWithDiagnostics({ 63 | selectionText: document.getText(selection), 64 | selectionStartLine: range.start.line, 65 | diagnostics, 66 | }); 67 | }; 68 | 69 | export type DiagnosticInRange = { 70 | code?: string | number | undefined; 71 | source?: string | undefined; 72 | message: string; 73 | line: number; 74 | severity: vscode.DiagnosticSeverity; 75 | }; 76 | 77 | function annotateSelectionWithDiagnostics({ 78 | selectionText, 79 | selectionStartLine, 80 | diagnostics, 81 | }: { 82 | selectionText: string; 83 | selectionStartLine: number; 84 | diagnostics: Array; 85 | }) { 86 | return selectionText 87 | .split(/[\r\n]+/) 88 | .map((line, index) => { 89 | const actualLineNumber = selectionStartLine + index; 90 | const lineDiagnostics = diagnostics.filter( 91 | (diagnostic) => diagnostic.line === actualLineNumber 92 | ); 93 | 94 | return lineDiagnostics.length === 0 95 | ? line 96 | : `${line}\n${lineDiagnostics 97 | .map( 98 | (diagnostic) => 99 | `${getLabel(diagnostic.severity)} ${diagnostic.source}${ 100 | diagnostic.code 101 | }: ${diagnostic.message}` 102 | ) 103 | .join("\n")}`; 104 | }) 105 | .join("\n"); 106 | } 107 | 108 | function getLabel(severity: vscode.DiagnosticSeverity) { 109 | switch (severity) { 110 | case vscode.DiagnosticSeverity.Error: 111 | return "ERROR"; 112 | case vscode.DiagnosticSeverity.Warning: 113 | return "WARNING"; 114 | case vscode.DiagnosticSeverity.Information: 115 | return "INFO"; 116 | case vscode.DiagnosticSeverity.Hint: 117 | return "HINT"; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/resolveVariable.ts: -------------------------------------------------------------------------------- 1 | import { getFilename } from "./getFilename"; 2 | import { getLanguage } from "./getLanguage"; 3 | import { getSelectedText } from "./getSelectedText"; 4 | import { Variable } from "../template/RubberduckTemplate"; 5 | import { Message } from "../Message"; 6 | import { getSelectedLocationText } from "./getSelectedLocationText"; 7 | import { getSelectedTextWithDiagnostics } from "./getSelectionWithDiagnostics"; 8 | import { getOpenFiles } from "./getOpenFiles"; 9 | 10 | export async function resolveVariable( 11 | variable: Variable, 12 | { messages }: { messages?: Array } = {} 13 | ): Promise { 14 | const variableType = variable.type; 15 | switch (variableType) { 16 | case "context": 17 | return getOpenFiles(); 18 | case "constant": 19 | return variable.value; 20 | case "message": 21 | return messages?.at(variable.index)?.[variable.property]; 22 | case "selected-text": 23 | return getSelectedText(); 24 | case "selected-location-text": 25 | return getSelectedLocationText(); 26 | case "filename": 27 | return getFilename(); 28 | case "language": 29 | return getLanguage(); 30 | case "selected-text-with-diagnostics": 31 | return getSelectedTextWithDiagnostics({ 32 | diagnosticSeverities: variable.severities, 33 | }); 34 | default: { 35 | const exhaustiveCheck: never = variableType; 36 | throw new Error(`unsupported type: ${exhaustiveCheck}`); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/resolveVariables.ts: -------------------------------------------------------------------------------- 1 | import { webviewApi } from "@rubberduck/common"; 2 | import { Variable } from "../template/RubberduckTemplate"; 3 | import { resolveVariable } from "./resolveVariable"; 4 | import { validateVariable } from "./validateVariable"; 5 | 6 | export async function resolveVariables( 7 | variables: Array | undefined, 8 | { 9 | time, 10 | messages, 11 | }: { 12 | time: Variable["time"]; 13 | messages?: Array; 14 | } 15 | ) { 16 | const variableValues: Record = { 17 | messages, 18 | }; 19 | 20 | // messages is a special variable that is always available: 21 | if (messages != null) { 22 | variableValues.messages = messages; 23 | } 24 | 25 | for (const variable of variables ?? []) { 26 | if (variable.time !== time) { 27 | continue; 28 | } 29 | 30 | if (variableValues[variable.name] != undefined) { 31 | throw new Error(`Variable '${variable.name}' is already defined`); 32 | } 33 | 34 | const value = await resolveVariable(variable, { messages }); 35 | 36 | validateVariable({ value, variable }); 37 | 38 | variableValues[variable.name] = value; 39 | } 40 | 41 | return variableValues; 42 | } 43 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/input/validateVariable.ts: -------------------------------------------------------------------------------- 1 | import { Variable } from "../template/RubberduckTemplate"; 2 | 3 | export function validateVariable({ 4 | value, 5 | variable, 6 | }: { 7 | value: unknown; 8 | variable: Variable; 9 | }) { 10 | for (const constraint of variable.constraints ?? []) { 11 | if (constraint.type === "text-length") { 12 | if (value == undefined) { 13 | throw new Error(`Variable '${variable.name}' is undefined`); 14 | } 15 | 16 | if (typeof value !== "string") { 17 | throw new Error(`Variable '${variable.name}' is not a string`); 18 | } 19 | if (value.length < constraint.min) { 20 | throw new Error(`Variable '${variable.name}' is too short`); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/retrieval-augmentation/EmbeddingFile.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | const chunkSchema = zod.object({ 4 | start_position: zod.number(), 5 | end_position: zod.number(), 6 | content: zod.string(), 7 | file: zod.string(), 8 | embedding: zod.array(zod.number()), 9 | }); 10 | 11 | export type ChunkWithContent = zod.infer; 12 | 13 | export const embeddingFileSchema = zod.object({ 14 | version: zod.literal(0), 15 | embedding: zod.object({ 16 | source: zod.literal("openai"), 17 | model: zod.literal("text-embedding-ada-002"), 18 | }), 19 | chunks: zod.array(chunkSchema), 20 | }); 21 | 22 | export type EmbeddingFile = zod.infer; 23 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/retrieval-augmentation/cosineSimilarity.ts: -------------------------------------------------------------------------------- 1 | export function cosineSimilarity(a: number[], b: number[]) { 2 | return dotProduct(a, b) / (magnitude(a) * magnitude(b)); 3 | } 4 | 5 | function dotProduct(a: number[], b: number[]) { 6 | return a.reduce( 7 | (acc: number, val: number, i: number) => acc + val * b[i]!, 8 | 0 9 | ); 10 | } 11 | 12 | function magnitude(a: number[]) { 13 | return Math.sqrt(dotProduct(a, a)); 14 | } 15 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/retrieval-augmentation/executeRetrievalAugmentation.ts: -------------------------------------------------------------------------------- 1 | import Handlebars from "handlebars"; 2 | import secureJSON from "secure-json-parse"; 3 | import * as vscode from "vscode"; 4 | import { AIClient } from "../../ai/AIClient"; 5 | import { readFileContent } from "../../vscode/readFileContent"; 6 | import { RetrievalAugmentation } from "../template/RubberduckTemplate"; 7 | import { cosineSimilarity } from "./cosineSimilarity"; 8 | import { embeddingFileSchema } from "./EmbeddingFile"; 9 | 10 | export async function executeRetrievalAugmentation({ 11 | retrievalAugmentation, 12 | initVariables, 13 | variables, 14 | ai, 15 | }: { 16 | retrievalAugmentation: RetrievalAugmentation; 17 | initVariables: Record; 18 | variables: Record; 19 | ai: AIClient; 20 | }): Promise< 21 | | Array<{ 22 | file: string; 23 | startPosition: number; 24 | endPosition: number; 25 | content: string; 26 | }> 27 | | undefined 28 | > { 29 | const file = retrievalAugmentation.file; 30 | 31 | const fileUri = vscode.Uri.joinPath( 32 | vscode.workspace.workspaceFolders?.[0]?.uri ?? vscode.Uri.file(""), 33 | ".rubberduck/embedding", 34 | file 35 | ); 36 | 37 | const fileContent = await readFileContent(fileUri); 38 | const parsedContent = secureJSON.parse(fileContent); 39 | const { chunks } = embeddingFileSchema.parse(parsedContent); 40 | 41 | // expand query with variables: 42 | const query = Handlebars.compile(retrievalAugmentation.query, { 43 | noEscape: true, 44 | })({ 45 | ...initVariables, 46 | ...variables, 47 | }); 48 | 49 | const result = await ai.generateEmbedding({ 50 | input: query, 51 | }); 52 | 53 | if (result.type === "error") { 54 | console.log(result.errorMessage); 55 | return undefined; 56 | } 57 | 58 | const queryEmbedding = result.embedding!; 59 | 60 | const similarityChunks = chunks 61 | .map(({ start_position, end_position, content, file, embedding }) => ({ 62 | file, 63 | startPosition: start_position, 64 | endPosition: end_position, 65 | content, 66 | similarity: cosineSimilarity(embedding, queryEmbedding), 67 | })) 68 | .filter(({ similarity }) => similarity >= retrievalAugmentation.threshold); 69 | 70 | similarityChunks.sort((a, b) => b.similarity - a.similarity); 71 | 72 | return similarityChunks 73 | .slice(0, retrievalAugmentation.maxResults) 74 | .map((chunk) => ({ 75 | file: chunk.file, 76 | startPosition: chunk.startPosition, 77 | endPosition: chunk.endPosition, 78 | content: chunk.content, 79 | })); 80 | } 81 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/template/RubberduckTemplate.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | const completionHandlerSchema = zod.discriminatedUnion("type", [ 4 | zod.object({ 5 | type: zod.literal("message"), 6 | }), 7 | zod.object({ 8 | type: zod.literal("update-temporary-editor"), 9 | botMessage: zod.string(), 10 | language: zod.string().optional(), 11 | }), 12 | zod.object({ 13 | type: zod.literal("active-editor-diff"), 14 | }), 15 | ]); 16 | 17 | const retrievalAugmentationSchema = zod.object({ 18 | variableName: zod.string(), 19 | type: zod.literal("similarity-search"), 20 | source: zod.literal("embedding-file"), 21 | file: zod.string(), 22 | query: zod.string(), 23 | threshold: zod.number().min(0).max(1), 24 | maxResults: zod.number().int().min(1), 25 | }); 26 | 27 | export type RetrievalAugmentation = zod.infer< 28 | typeof retrievalAugmentationSchema 29 | >; 30 | 31 | const promptSchema = zod.object({ 32 | placeholder: zod.string().optional(), 33 | retrievalAugmentation: retrievalAugmentationSchema.optional(), 34 | maxTokens: zod.number(), 35 | stop: zod.array(zod.string()).optional(), 36 | temperature: zod.number().optional(), 37 | completionHandler: completionHandlerSchema.optional(), 38 | }); 39 | 40 | export type Prompt = zod.infer & { 41 | /** 42 | * Resolved template. 43 | */ 44 | template: string; 45 | }; 46 | 47 | const variableBaseSchema = zod.object({ 48 | name: zod.string(), 49 | constraints: zod 50 | .array( 51 | zod.discriminatedUnion("type", [ 52 | zod.object({ 53 | type: zod.literal("text-length"), 54 | min: zod.number(), 55 | }), 56 | ]) 57 | ) 58 | .optional(), 59 | }); 60 | 61 | const variableSchema = zod.discriminatedUnion("type", [ 62 | variableBaseSchema.extend({ 63 | type: zod.literal("constant"), 64 | time: zod.literal("conversation-start"), 65 | value: zod.string(), 66 | }), 67 | variableBaseSchema.extend({ 68 | type: zod.literal("message"), 69 | time: zod.literal("message"), 70 | index: zod.number(), 71 | property: zod.enum(["content"]), 72 | }), 73 | variableBaseSchema.extend({ 74 | type: zod.literal("context"), 75 | time: zod.enum(["conversation-start"]), 76 | }), 77 | variableBaseSchema.extend({ 78 | type: zod.literal("selected-text"), 79 | time: zod.enum(["conversation-start", "message"]), 80 | }), 81 | variableBaseSchema.extend({ 82 | type: zod.literal("selected-location-text"), 83 | time: zod.enum(["conversation-start"]), 84 | }), 85 | variableBaseSchema.extend({ 86 | type: zod.literal("filename"), 87 | time: zod.enum(["conversation-start"]), 88 | }), 89 | variableBaseSchema.extend({ 90 | type: zod.literal("language"), 91 | time: zod.enum(["conversation-start"]), 92 | }), 93 | variableBaseSchema.extend({ 94 | type: zod.literal("selected-text-with-diagnostics"), 95 | time: zod.literal("conversation-start"), 96 | severities: zod.array( 97 | zod.enum(["error", "warning", "information", "hint"]) 98 | ), 99 | }), 100 | ]); 101 | 102 | export type Variable = zod.infer; 103 | 104 | export const rubberduckTemplateSchema = zod.object({ 105 | id: zod.string(), 106 | engineVersion: zod.literal(0), 107 | label: zod.string(), 108 | description: zod.string(), 109 | tags: zod.array(zod.string()).optional(), 110 | header: zod.object({ 111 | title: zod.string(), 112 | useFirstMessageAsTitle: zod.boolean().optional(), // default: false 113 | icon: zod.object({ 114 | type: zod.literal("codicon"), 115 | value: zod.string(), 116 | }), 117 | }), 118 | chatInterface: zod 119 | .enum(["message-exchange", "instruction-refinement"]) 120 | .optional(), // default: message-exchange 121 | isEnabled: zod.boolean().optional(), // default: true 122 | variables: zod.array(variableSchema).optional(), 123 | initialMessage: promptSchema.optional(), 124 | response: promptSchema, 125 | }); 126 | 127 | export type RubberduckTemplate = zod.infer & { 128 | initialMessage?: Prompt; 129 | response: Prompt; 130 | }; 131 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/template/RubberduckTemplateLoadResult.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { RubberduckTemplate } from "./RubberduckTemplate"; 3 | 4 | export type RubberduckTemplateLoadResult = 5 | | { 6 | type: "success"; 7 | file: vscode.Uri; 8 | template: RubberduckTemplate; 9 | } 10 | | { 11 | type: "error"; 12 | file: vscode.Uri; 13 | error: unknown; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/template/loadRubberduckTemplateFromFile.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { RubberduckTemplateLoadResult } from "./RubberduckTemplateLoadResult"; 3 | import { parseRubberduckTemplate } from "./parseRubberduckTemplate"; 4 | import { readFileContent } from "../../vscode/readFileContent"; 5 | 6 | export const loadConversationFromFile = async ( 7 | file: vscode.Uri 8 | ): Promise => { 9 | try { 10 | const parseResult = parseRubberduckTemplate(await readFileContent(file)); 11 | 12 | if (parseResult.type === "error") { 13 | return { 14 | type: "error" as const, 15 | file, 16 | error: parseResult.error, 17 | }; 18 | } 19 | 20 | return { 21 | type: "success" as const, 22 | file, 23 | template: parseResult.template, 24 | }; 25 | } catch (error) { 26 | return { 27 | type: "error" as const, 28 | file, 29 | error, 30 | }; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/template/loadRubberduckTemplatesFromWorkspace.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { loadConversationFromFile } from "./loadRubberduckTemplateFromFile"; 3 | import { RubberduckTemplateLoadResult } from "./RubberduckTemplateLoadResult"; 4 | 5 | const TEMPLATE_GLOB = ".rubberduck/template/**/*.rdt.md"; 6 | 7 | export async function loadRubberduckTemplatesFromWorkspace(): Promise< 8 | Array 9 | > { 10 | const files = await vscode.workspace.findFiles(TEMPLATE_GLOB); 11 | return await Promise.all(files.map(loadConversationFromFile)); 12 | } 13 | -------------------------------------------------------------------------------- /lib/extension/src/conversation/template/parseRubberduckTemplate.ts: -------------------------------------------------------------------------------- 1 | import { marked } from "marked"; 2 | import secureJSON from "secure-json-parse"; 3 | import { 4 | RubberduckTemplate, 5 | rubberduckTemplateSchema, 6 | Prompt, 7 | } from "./RubberduckTemplate"; 8 | 9 | export type RubberduckTemplateParseResult = 10 | | { 11 | type: "success"; 12 | template: RubberduckTemplate; 13 | } 14 | | { 15 | type: "error"; 16 | error: unknown; 17 | }; 18 | 19 | class NamedCodeSnippetMap { 20 | private readonly contentByLangInfo = new Map(); 21 | 22 | set(langInfo: string, content: string): void { 23 | this.contentByLangInfo.set(langInfo, content); 24 | } 25 | 26 | get(langInfo: string): string { 27 | const content = this.contentByLangInfo.get(langInfo); 28 | 29 | if (content == null) { 30 | throw new Error(`Code snippet for lang info '${langInfo}' not found.`); 31 | } 32 | 33 | return content; 34 | } 35 | 36 | resolveTemplate(prompt: Prompt, templateId: string) { 37 | prompt.template = this.getHandlebarsTemplate(templateId); 38 | } 39 | 40 | private getHandlebarsTemplate(templateName: string): string { 41 | return this.get(`template-${templateName}`).replace(/\\`\\`\\`/g, "```"); 42 | } 43 | } 44 | 45 | export const extractNamedCodeSnippets = ( 46 | content: string 47 | ): NamedCodeSnippetMap => { 48 | const codeSnippets = new NamedCodeSnippetMap(); 49 | 50 | marked 51 | .lexer(content) 52 | .filter((token) => token.type === "code") 53 | .forEach((token) => { 54 | const codeToken = token as marked.Tokens.Code; 55 | if (codeToken.lang != null) { 56 | codeSnippets.set(codeToken.lang, codeToken.text); 57 | } 58 | }); 59 | 60 | return codeSnippets; 61 | }; 62 | 63 | export function parseRubberduckTemplateOrThrow( 64 | templateAsRdtMarkdown: string 65 | ): RubberduckTemplate { 66 | const parseResult = parseRubberduckTemplate(templateAsRdtMarkdown); 67 | 68 | if (parseResult.type === "error") { 69 | throw parseResult.error; 70 | } 71 | 72 | return parseResult.template; 73 | } 74 | 75 | export function parseRubberduckTemplate( 76 | templateAsRdtMarkdown: string 77 | ): RubberduckTemplateParseResult { 78 | try { 79 | const namedCodeSnippets = extractNamedCodeSnippets(templateAsRdtMarkdown); 80 | 81 | const templateText = namedCodeSnippets.get("json conversation-template"); 82 | 83 | const template = rubberduckTemplateSchema.parse( 84 | secureJSON.parse(templateText) 85 | ); 86 | 87 | if (template.initialMessage != null) { 88 | namedCodeSnippets.resolveTemplate( 89 | template.initialMessage as Prompt, 90 | "initial-message" 91 | ); 92 | } 93 | 94 | namedCodeSnippets.resolveTemplate(template.response as Prompt, "response"); 95 | 96 | return { 97 | type: "success", 98 | template: template as RubberduckTemplate, 99 | }; 100 | } catch (error) { 101 | return { 102 | type: "error", 103 | error, 104 | }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/extension/src/diff/DiffEditor.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { WebviewContainer } from "../webview/WebviewContainer"; 3 | 4 | export class DiffEditor { 5 | private container: WebviewContainer; 6 | 7 | private messageEmitter = new vscode.EventEmitter(); 8 | private messageHandler: vscode.Disposable | undefined; 9 | 10 | constructor({ 11 | title, 12 | editorColumn, 13 | extensionUri, 14 | conversationId, 15 | }: { 16 | title: string; 17 | editorColumn: vscode.ViewColumn; 18 | extensionUri: vscode.Uri; 19 | conversationId: string; 20 | }) { 21 | const panel = vscode.window.createWebviewPanel( 22 | `rubberduck.diff.${conversationId}`, 23 | title, 24 | editorColumn 25 | ); 26 | 27 | const useVisualStudioCodeColors: boolean = vscode.workspace 28 | .getConfiguration("rubberduck.syntaxHighlighting") 29 | .get("useVisualStudioCodeColors", false); 30 | 31 | this.container = new WebviewContainer({ 32 | panelId: "diff", 33 | panelCssId: useVisualStudioCodeColors 34 | ? "diff-vscode-colors" 35 | : "diff-hardcoded-colors", 36 | isStateReloadingEnabled: true, 37 | webview: panel.webview, 38 | extensionUri, 39 | }); 40 | 41 | this.container.onDidReceiveMessage((message: unknown) => { 42 | this.messageEmitter.fire(message); 43 | }); 44 | } 45 | 46 | onDidReceiveMessage: vscode.Event = ( 47 | listener, 48 | thisArg, 49 | disposables 50 | ) => { 51 | // We only want to execute the last listener to apply the latest change. 52 | this.messageHandler?.dispose(); 53 | this.messageHandler = this.messageEmitter.event( 54 | listener, 55 | thisArg, 56 | disposables 57 | ); 58 | return this.messageHandler; 59 | }; 60 | 61 | async updateDiff({ 62 | oldCode, 63 | newCode, 64 | languageId, 65 | }: { 66 | oldCode: string; 67 | newCode: string; 68 | languageId?: string; 69 | }) { 70 | await this.container.updateState({ 71 | type: "diff", 72 | oldCode, 73 | newCode, 74 | languageId, 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/extension/src/diff/DiffEditorManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { DiffEditor } from "./DiffEditor"; 3 | 4 | export class DiffEditorManager { 5 | private extensionUri: vscode.Uri; 6 | 7 | constructor({ extensionUri }: { extensionUri: vscode.Uri }) { 8 | this.extensionUri = extensionUri; 9 | } 10 | 11 | createDiffEditor({ 12 | title, 13 | editorColumn, 14 | conversationId, 15 | }: { 16 | title: string; 17 | editorColumn: vscode.ViewColumn; 18 | conversationId: string; 19 | }) { 20 | return new DiffEditor({ 21 | title, 22 | editorColumn, 23 | extensionUri: this.extensionUri, 24 | conversationId, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AIClient } from "./ai/AIClient"; 3 | import { ApiKeyManager } from "./ai/ApiKeyManager"; 4 | import { ChatController } from "./chat/ChatController"; 5 | import { ChatModel } from "./chat/ChatModel"; 6 | import { ChatPanel } from "./chat/ChatPanel"; 7 | import { ConversationTypesProvider } from "./conversation/ConversationTypesProvider"; 8 | import { DiffEditorManager } from "./diff/DiffEditorManager"; 9 | import { indexRepository } from "./index/indexRepository"; 10 | import { getVSCodeLogLevel, LoggerUsingVSCodeOutput } from "./logger"; 11 | 12 | export const activate = async (context: vscode.ExtensionContext) => { 13 | const apiKeyManager = new ApiKeyManager({ 14 | secretStorage: context.secrets, 15 | }); 16 | 17 | const mainOutputChannel = vscode.window.createOutputChannel("Rubberduck"); 18 | const indexOutputChannel = 19 | vscode.window.createOutputChannel("Rubberduck Index"); 20 | 21 | const vscodeLogger = new LoggerUsingVSCodeOutput({ 22 | outputChannel: mainOutputChannel, 23 | level: getVSCodeLogLevel(), 24 | }); 25 | vscode.workspace.onDidChangeConfiguration((event) => { 26 | if (event.affectsConfiguration("rubberduck.logger.level")) { 27 | vscodeLogger.setLevel(getVSCodeLogLevel()); 28 | } 29 | }); 30 | 31 | const hasOpenAIApiKey = await apiKeyManager.hasOpenAIApiKey(); 32 | const chatPanel = new ChatPanel({ 33 | extensionUri: context.extensionUri, 34 | apiKeyManager, 35 | hasOpenAIApiKey, 36 | }); 37 | 38 | const chatModel = new ChatModel(); 39 | 40 | const conversationTypesProvider = new ConversationTypesProvider({ 41 | extensionUri: context.extensionUri, 42 | }); 43 | 44 | await conversationTypesProvider.loadConversationTypes(); 45 | 46 | const ai = new AIClient({ 47 | apiKeyManager, 48 | logger: vscodeLogger, 49 | }); 50 | 51 | const chatController = new ChatController({ 52 | chatPanel, 53 | chatModel, 54 | ai, 55 | diffEditorManager: new DiffEditorManager({ 56 | extensionUri: context.extensionUri, 57 | }), 58 | getConversationType(id: string) { 59 | return conversationTypesProvider.getConversationType(id); 60 | }, 61 | basicChatTemplateId: "chat-en", 62 | }); 63 | 64 | chatPanel.onDidReceiveMessage( 65 | chatController.receivePanelMessage.bind(chatController) 66 | ); 67 | 68 | context.subscriptions.push( 69 | vscode.window.registerWebviewViewProvider("rubberduck.chat", chatPanel), 70 | vscode.commands.registerCommand( 71 | "rubberduck.enterOpenAIApiKey", 72 | apiKeyManager.enterOpenAIApiKey.bind(apiKeyManager) 73 | ), 74 | vscode.commands.registerCommand( 75 | "rubberduck.clearOpenAIApiKey", 76 | async () => { 77 | await apiKeyManager.clearOpenAIApiKey(); 78 | vscode.window.showInformationMessage("OpenAI API key cleared."); 79 | } 80 | ), 81 | 82 | vscode.commands.registerCommand( 83 | "rubberduck.startConversation", 84 | (templateId) => chatController.createConversation(templateId) 85 | ), 86 | 87 | vscode.commands.registerCommand("rubberduck.diagnoseErrors", () => { 88 | chatController.createConversation("diagnose-errors"); 89 | }), 90 | vscode.commands.registerCommand("rubberduck.explainCode", () => { 91 | chatController.createConversation("explain-code"); 92 | }), 93 | vscode.commands.registerCommand("rubberduck.findBugs", () => { 94 | chatController.createConversation("find-bugs"); 95 | }), 96 | vscode.commands.registerCommand("rubberduck.generateCode", () => { 97 | chatController.createConversation("generate-code"); 98 | }), 99 | vscode.commands.registerCommand("rubberduck.generateUnitTest", () => { 100 | chatController.createConversation("generate-unit-test"); 101 | }), 102 | vscode.commands.registerCommand("rubberduck.startChat", () => { 103 | chatController.createConversation("chat-en"); 104 | }), 105 | vscode.commands.registerCommand("rubberduck.editCode", () => { 106 | chatController.createConversation("edit-code"); 107 | }), 108 | vscode.commands.registerCommand("rubberduck.startCustomChat", async () => { 109 | const items = conversationTypesProvider 110 | .getConversationTypes() 111 | .map((conversationType) => ({ 112 | id: conversationType.id, 113 | label: conversationType.label, 114 | description: (() => { 115 | const tags = conversationType.tags; 116 | return tags == null 117 | ? conversationType.source 118 | : `${conversationType.source}, ${tags.join(", ")}`; 119 | })(), 120 | detail: conversationType.description, 121 | })); 122 | 123 | const result = await vscode.window.showQuickPick(items, { 124 | title: `Start Custom Chat…`, 125 | matchOnDescription: true, 126 | matchOnDetail: true, 127 | placeHolder: "Select conversation type…", 128 | }); 129 | 130 | if (result == undefined) { 131 | return; // user cancelled 132 | } 133 | 134 | await chatController.createConversation(result.id); 135 | }), 136 | vscode.commands.registerCommand("rubberduck.touchBar.startChat", () => { 137 | chatController.createConversation("chat-en"); 138 | }), 139 | vscode.commands.registerCommand("rubberduck.showChatPanel", async () => { 140 | await chatController.showChatPanel(); 141 | }), 142 | vscode.commands.registerCommand("rubberduck.getStarted", async () => { 143 | await vscode.commands.executeCommand("workbench.action.openWalkthrough", { 144 | category: `rubberduck.rubberduck-vscode#rubberduck`, 145 | }); 146 | }), 147 | vscode.commands.registerCommand("rubberduck.reloadTemplates", async () => { 148 | await conversationTypesProvider.loadConversationTypes(); 149 | vscode.window.showInformationMessage("Rubberduck templates reloaded."); 150 | }), 151 | 152 | vscode.commands.registerCommand("rubberduck.showLogs", () => { 153 | mainOutputChannel.show(true); 154 | }), 155 | 156 | vscode.commands.registerCommand("rubberduck.indexRepository", () => { 157 | indexRepository({ 158 | ai: ai, 159 | outputChannel: indexOutputChannel, 160 | }); 161 | }) 162 | ); 163 | 164 | return Object.freeze({ 165 | async registerTemplate({ template }: { template: string }) { 166 | conversationTypesProvider.registerExtensionTemplate({ template }); 167 | await conversationTypesProvider.loadConversationTypes(); 168 | }, 169 | }); 170 | }; 171 | 172 | export const deactivate = async () => { 173 | // noop 174 | }; 175 | -------------------------------------------------------------------------------- /lib/extension/src/index/chunk/Chunk.ts: -------------------------------------------------------------------------------- 1 | export type Chunk = { 2 | startPosition: number; 3 | endPosition: number; 4 | content: string; 5 | }; 6 | -------------------------------------------------------------------------------- /lib/extension/src/index/chunk/calculateLinePositions.ts: -------------------------------------------------------------------------------- 1 | export type LinePosition = { 2 | start: number; 3 | end: number; 4 | }; 5 | 6 | export function calculateLinePositions(lines: string[], lineSeparator: string) { 7 | const linePositions: Array = []; 8 | 9 | let position = 0; 10 | 11 | for (const line of lines) { 12 | linePositions.push({ 13 | start: position, 14 | end: position + line.length, // note: separator is not included 15 | }); 16 | 17 | position += line.length + lineSeparator.length; 18 | } 19 | 20 | return linePositions; 21 | } 22 | -------------------------------------------------------------------------------- /lib/extension/src/index/chunk/splitLinearLines.ts: -------------------------------------------------------------------------------- 1 | import { calculateLinePositions } from "./calculateLinePositions"; 2 | import { Chunk } from "./Chunk"; 3 | 4 | type Segment = { 5 | lines: string[]; 6 | startLine: number; 7 | characterCount: number; 8 | }; 9 | 10 | export function createSplitLinearLines({ 11 | maxChunkCharacters, 12 | lineSeparator = "\n", 13 | }: { 14 | maxChunkCharacters: number; 15 | lineSeparator?: string | undefined; 16 | }) { 17 | return function splitLinearLines(content: string): Array { 18 | const lines = content.split(lineSeparator); 19 | const linePositions = calculateLinePositions(lines, lineSeparator); 20 | 21 | const chunks: Array = []; 22 | 23 | let segment: Segment | undefined = undefined; 24 | 25 | function addSegmentToChunks(currentLine: number) { 26 | if (segment == undefined) { 27 | return; 28 | } 29 | 30 | chunks.push({ 31 | startPosition: linePositions[segment.startLine]!.start, 32 | endPosition: linePositions[currentLine]!.end, 33 | content: segment.lines.join(lineSeparator), 34 | }); 35 | 36 | segment = undefined; 37 | } 38 | 39 | for (let i = 0; i < lines.length; i++) { 40 | const lineText = lines[i]!; 41 | 42 | if (segment == null) { 43 | segment = { 44 | lines: [lineText], 45 | startLine: i, 46 | characterCount: lineText.length, 47 | }; 48 | } else { 49 | segment.lines.push(lineText); 50 | segment.characterCount += lineText.length + lineSeparator.length; 51 | } 52 | 53 | // this leads to chunks that are too big (by 1 line) 54 | if (segment.characterCount > maxChunkCharacters) { 55 | addSegmentToChunks(i); 56 | } 57 | } 58 | 59 | addSegmentToChunks(lines.length - 1); 60 | 61 | return chunks; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /lib/extension/src/index/indexRepository.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import { simpleGit } from "simple-git"; 3 | import * as vscode from "vscode"; 4 | import { AIClient } from "../ai/AIClient"; 5 | import { ChunkWithContent } from "../conversation/retrieval-augmentation/EmbeddingFile"; 6 | import { createSplitLinearLines } from "./chunk/splitLinearLines"; 7 | 8 | export async function indexRepository({ 9 | ai, 10 | outputChannel, 11 | }: { 12 | ai: AIClient; 13 | outputChannel: vscode.OutputChannel; 14 | }) { 15 | const repositoryPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 16 | 17 | if (repositoryPath == undefined) { 18 | vscode.window.showErrorMessage("Rubberduck: No workspace folder is open."); 19 | return; 20 | } 21 | 22 | outputChannel.show(true); 23 | outputChannel.appendLine(`Indexing repository ${repositoryPath}`); 24 | 25 | const git = simpleGit({ 26 | baseDir: repositoryPath, 27 | binary: "git", 28 | maxConcurrentProcesses: 6, 29 | trimmed: false, 30 | }); 31 | 32 | const files = (await git.raw(["ls-files"])).split("\n"); 33 | const chunksWithEmbedding: Array = []; 34 | 35 | let tokenCount = 0; 36 | let cancelled = false; 37 | 38 | await vscode.window.withProgress( 39 | { 40 | location: vscode.ProgressLocation.Notification, 41 | title: "Indexing repository", 42 | cancellable: true, 43 | }, 44 | async (progress, cancellationToken) => { 45 | for (const file of files) { 46 | progress.report({ 47 | message: `Indexing ${file}`, 48 | increment: 100 / files.length, 49 | }); 50 | 51 | if (cancellationToken.isCancellationRequested) { 52 | cancelled = true; 53 | break; 54 | } 55 | 56 | if (!isSupportedFile(file)) { 57 | continue; 58 | } 59 | 60 | // TODO potential bug on windows 61 | const content = await fs.readFile(`${repositoryPath}/${file}`, "utf8"); 62 | 63 | const chunks = createSplitLinearLines({ 64 | maxChunkCharacters: 500, // ~4 char per token 65 | })(content); 66 | 67 | for (const chunk of chunks) { 68 | if (cancellationToken.isCancellationRequested) { 69 | cancelled = true; 70 | break; 71 | } 72 | 73 | outputChannel.appendLine( 74 | `Generating embedding for chunk '${file}' ${chunk.startPosition}:${chunk.endPosition}` 75 | ); 76 | 77 | try { 78 | const embeddingResult = await ai.generateEmbedding({ 79 | input: chunk.content, 80 | }); 81 | 82 | if (embeddingResult.type === "error") { 83 | outputChannel.appendLine( 84 | `Failed to generate embedding for chunk '${file}' ${chunk.startPosition}:${chunk.endPosition} - ${embeddingResult.errorMessage}}` 85 | ); 86 | 87 | console.error(embeddingResult.errorMessage); 88 | continue; 89 | } 90 | 91 | chunksWithEmbedding.push({ 92 | file, 93 | start_position: chunk.startPosition, 94 | end_position: chunk.endPosition, 95 | content: chunk.content, 96 | embedding: embeddingResult.embedding, 97 | }); 98 | 99 | tokenCount += embeddingResult?.totalTokenCount ?? 0; 100 | } catch (error) { 101 | console.error(error); 102 | 103 | outputChannel.appendLine( 104 | `Failed to generate embedding for chunk '${file}' ${chunk.startPosition}:${chunk.endPosition}` 105 | ); 106 | } 107 | } 108 | } 109 | } 110 | ); 111 | 112 | if (!cancelled) { 113 | // TODO potential bug on windows 114 | const filename = `${repositoryPath}/.rubberduck/embedding/repository.json`; 115 | 116 | // TODO potential bug on windows 117 | await fs.mkdir(`${repositoryPath}/.rubberduck/embedding`, { 118 | recursive: true, 119 | }); 120 | 121 | await fs.writeFile( 122 | filename, 123 | JSON.stringify({ 124 | version: 0, 125 | embedding: { 126 | source: "openai", 127 | model: "text-embedding-ada-002", 128 | }, 129 | chunks: chunksWithEmbedding, 130 | }) 131 | ); 132 | } 133 | 134 | outputChannel.appendLine(""); 135 | 136 | if (cancelled) { 137 | outputChannel.appendLine("Indexing cancelled"); 138 | } 139 | 140 | outputChannel.appendLine(`Tokens used: ${tokenCount}`); 141 | outputChannel.appendLine(`Cost: ${(tokenCount / 1000) * 0.0004} USD`); 142 | } 143 | 144 | function isSupportedFile(file: string) { 145 | return ( 146 | (file.endsWith(".js") || 147 | file.endsWith(".ts") || 148 | file.endsWith(".tsx") || 149 | file.endsWith(".sh") || 150 | file.endsWith(".yaml") || 151 | file.endsWith(".yml") || 152 | file.endsWith(".md") || 153 | file.endsWith(".css") || 154 | file.endsWith(".json") || 155 | file.endsWith(".toml") || 156 | file.endsWith(".config")) && 157 | !( 158 | file.endsWith(".min.js") || 159 | file.endsWith(".min.css") || 160 | file.endsWith("pnpm-lock.yaml") 161 | ) 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /lib/extension/src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** Log levels in increasing order of importance */ 4 | const logLevels = ["debug", "info", "warning", "error"] as const; 5 | type LogLevel = (typeof logLevels)[number]; 6 | 7 | export function getVSCodeLogLevel(): LogLevel { 8 | const setting: string = vscode.workspace 9 | .getConfiguration("rubberduck.logger") 10 | .get("level", ""); 11 | 12 | return logLevels.find((l) => setting == l) ?? "info"; 13 | } 14 | 15 | export interface Logger { 16 | setLevel(level: LogLevel): void; 17 | debug(message: string | string[]): void; 18 | log(message: string | string[]): void; 19 | warn(message: string | string[]): void; 20 | error(message: string | string[]): void; 21 | } 22 | 23 | export class LoggerUsingVSCodeOutput implements Logger { 24 | private level: LogLevel; 25 | private readonly outputChannel: vscode.OutputChannel; 26 | 27 | constructor({ 28 | level, 29 | outputChannel, 30 | }: { 31 | level: LogLevel; 32 | outputChannel: vscode.OutputChannel; 33 | }) { 34 | this.level = level; 35 | this.outputChannel = outputChannel; 36 | } 37 | 38 | setLevel(level: LogLevel) { 39 | this.level = level; 40 | } 41 | 42 | debug(message: string | string[]): void { 43 | return this.write({ 44 | lines: ([] as string[]).concat(message), 45 | prefix: "[DEBUG]", 46 | level: "debug", 47 | }); 48 | } 49 | 50 | log(message: string | string[]): void { 51 | return this.write({ 52 | lines: ([] as string[]).concat(message), 53 | prefix: "[INFO]", 54 | level: "info", 55 | }); 56 | } 57 | 58 | warn(message: string | string[]): void { 59 | return this.write({ 60 | lines: ([] as string[]).concat(message), 61 | prefix: "[WARNING]", 62 | level: "warning", 63 | }); 64 | } 65 | 66 | error(message: string | string[]): void { 67 | return this.write({ 68 | lines: ([] as string[]).concat(message), 69 | prefix: "[ERROR]", 70 | level: "error", 71 | }); 72 | } 73 | 74 | private write(options: { 75 | lines: string[]; 76 | prefix: string; 77 | level: LogLevel; 78 | }): void { 79 | const { lines, prefix, level } = options; 80 | if (!this.canLog(level)) return; 81 | 82 | lines.forEach((line) => { 83 | this.outputChannel.appendLine(`${prefix} ${line}`); 84 | }); 85 | } 86 | 87 | private canLog(level: LogLevel): boolean { 88 | const requestedLevel = logLevels.findIndex((l) => l == level); 89 | const minLevel = logLevels.findIndex((l) => l == this.level); 90 | return requestedLevel >= minLevel; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/extension/src/vscode/getActiveEditor.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export function getActiveEditor() { 4 | return vscode.window.activeTextEditor ?? vscode.window.visibleTextEditors[0]; 5 | } 6 | -------------------------------------------------------------------------------- /lib/extension/src/vscode/readFileContent.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export async function readFileContent(file: vscode.Uri) { 4 | const data = await vscode.workspace.fs.readFile(file); 5 | return Buffer.from(data).toString("utf8"); 6 | } 7 | -------------------------------------------------------------------------------- /lib/extension/src/webview/WebviewContainer.ts: -------------------------------------------------------------------------------- 1 | import { webviewApi } from "@rubberduck/common"; 2 | import * as vscode from "vscode"; 3 | import { generateNonce } from "./generateNonce"; 4 | 5 | export class WebviewContainer { 6 | private readonly webview: vscode.Webview; 7 | private readonly panelId: string; 8 | private readonly panelCssId: string; 9 | private readonly extensionUri: vscode.Uri; 10 | private readonly isStateReloadingEnabled: boolean; 11 | 12 | readonly onDidReceiveMessage; 13 | 14 | constructor({ 15 | panelId, 16 | panelCssId = panelId, 17 | webview, 18 | extensionUri, 19 | isStateReloadingEnabled, 20 | }: { 21 | panelId: "chat" | "diff"; 22 | panelCssId?: string; 23 | webview: vscode.Webview; 24 | extensionUri: vscode.Uri; 25 | isStateReloadingEnabled: boolean; 26 | }) { 27 | this.panelId = panelId; 28 | this.panelCssId = panelCssId; 29 | this.webview = webview; 30 | this.extensionUri = extensionUri; 31 | this.isStateReloadingEnabled = isStateReloadingEnabled; 32 | 33 | this.webview.options = { 34 | enableScripts: true, 35 | localResourceRoots: [this.extensionUri], 36 | }; 37 | this.webview.html = this.createHtml(); 38 | 39 | this.onDidReceiveMessage = this.webview.onDidReceiveMessage; 40 | } 41 | 42 | async updateState(state: webviewApi.PanelState) { 43 | return this.webview.postMessage({ 44 | type: "updateState", 45 | state, 46 | } satisfies webviewApi.IncomingMessage["data"]); 47 | } 48 | 49 | private getUri(...paths: string[]) { 50 | return this.webview.asWebviewUri( 51 | vscode.Uri.joinPath(this.extensionUri, "webview", ...paths) 52 | ); 53 | } 54 | 55 | private createHtml() { 56 | const baseCssUri = this.getUri("asset", "base.css"); 57 | const codiconsCssUri = this.getUri("asset", "codicons.css"); 58 | const panelCssUri = this.getUri("asset", `${this.panelCssId}.css`); 59 | const scriptUri = this.getUri("dist", "webview.js"); 60 | const prismScriptUri = this.getUri("asset", "prism.js"); 61 | 62 | // Use a nonce to only allow a specific script to be run. 63 | const nonce = generateNonce(); 64 | const prismNonce = generateNonce(); 65 | 66 | const cspSource = this.webview?.cspSource; 67 | 68 | return ` 69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 84 |