├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE.txt ├── README.md ├── assets ├── codify.woff ├── custom-theme.css └── hl.min.js ├── contributing.md ├── global.d.ts ├── images ├── add_line.svg ├── discussion-bubble.svg ├── logo-small.png ├── refact-logo.png ├── refact-logo.svg ├── refact-wide.png └── remove_line.svg ├── logo-small.png ├── package.json ├── src ├── chatTab.ts ├── codeLens.ts ├── completionProvider.ts ├── crlf.ts ├── estate.ts ├── extension.ts ├── fetchAPI.ts ├── getKeybindings.ts ├── interactiveDiff.ts ├── launchRust.ts ├── quickProvider.ts ├── rconsoleCommands.ts ├── rconsoleProvider.ts ├── sidebar.ts ├── statisticTab.ts ├── statusBar.ts ├── storeVersions.ts ├── usabilityHints.ts └── userLogin.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI build 2 | 3 | on: 4 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) 5 | push: 6 | branches: [main, dev] 7 | # Trigger the workflow on any pull request 8 | pull_request: 9 | workflow_dispatch: 10 | inputs: 11 | lsp_branch: 12 | description: "Branch name of lsp" 13 | default: "main" 14 | chat_js_branch: 15 | description: "Branch name of chat-js" 16 | default: "main" 17 | 18 | jobs: 19 | dist: 20 | strategy: 21 | matrix: 22 | include: 23 | - os: windows-latest 24 | target: x86_64-pc-windows-msvc 25 | code-target: win32-x64 26 | - os: windows-latest 27 | target: aarch64-pc-windows-msvc 28 | code-target: win32-arm64 29 | - os: ubuntu-22.04 30 | target: x86_64-unknown-linux-gnu 31 | code-target: linux-x64 32 | - os: ubuntu-22.04 33 | target: aarch64-unknown-linux-gnu 34 | code-target: linux-arm64 35 | # - os: ubuntu-20.04 36 | # target: armv7-unknown-linux-gnueabihf 37 | # code-target: linux-armhf 38 | - os: macos-13 39 | target: x86_64-apple-darwin 40 | code-target: darwin-x64 41 | - os: macos-14 42 | target: aarch64-apple-darwin 43 | code-target: darwin-arm64 44 | 45 | env: 46 | LLM_LS_TARGET: ${{ matrix.target }} 47 | 48 | name: dist (${{ matrix.target }}) 49 | runs-on: ${{ matrix.os }} 50 | container: ${{ matrix.container }} 51 | 52 | steps: 53 | - name: Setup node 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: 20 57 | 58 | - name: Checkout repository 59 | uses: actions/checkout@v4 60 | with: 61 | fetch-depth: ${{ env.FETCH_DEPTH }} 62 | 63 | - name: Download lsp artifacts 64 | id: download-artifact-lsp 65 | uses: dawidd6/action-download-artifact@v3 66 | with: 67 | github_token: ${{secrets.GITHUB_TOKEN}} 68 | workflow: agent_engine_build.yml 69 | repo: smallcloudai/refact 70 | branch: ${{ inputs.lsp_branch }} 71 | path: ./assets 72 | name: dist-${{ matrix.target }} 73 | 74 | - name: Download chat artifacts 75 | id: download-artifact-chat 76 | uses: dawidd6/action-download-artifact@v9 77 | with: 78 | github_token: ${{secrets.GITHUB_TOKEN}} 79 | workflow: agent_gui_build.yml 80 | repo: smallcloudai/refact 81 | branch: ${{ inputs.chat_js_branch }} 82 | path: ./chat_package 83 | name: lts-refact-chat-js-.*\.tgz 84 | name_is_regexp: true 85 | 86 | - name: Prepare chat package 87 | shell: bash 88 | run: | 89 | mkdir -p ./chat_package_fixed 90 | find ./chat_package -name "*.tgz" -type f -exec cp {} ./chat_package_fixed/ \; 91 | ls -la ./chat_package_fixed 92 | 93 | - name: Install VSCE 94 | shell: bash 95 | run: | 96 | npm install ./chat_package_fixed/*.tgz 97 | npm install -g @vscode/vsce 98 | rm -rf ./chat_package ./chat_package_fixed 99 | 100 | - name: Package VSCE extension 101 | run: | 102 | chmod +x ./assets/refact-lsp* 103 | vsce package --target ${{ matrix.code-target }} 104 | 105 | - name: Upload artifacts 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: vscode-plugin-${{ matrix.target }} 109 | path: ./*.vsix 110 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | lsp_branch: 7 | description: "Branch name of lsp" 8 | default: "main" 9 | chat_js_branch: 10 | description: "Branch name of chat-js" 11 | default: "main" 12 | stable_release: 13 | description: "Publish stable release version" 14 | default: false 15 | type: boolean 16 | 17 | jobs: 18 | publish: 19 | strategy: 20 | matrix: 21 | include: 22 | - os: windows-latest 23 | target: x86_64-pc-windows-msvc 24 | code-target: win32-x64 25 | - os: windows-latest 26 | target: aarch64-pc-windows-msvc 27 | code-target: win32-arm64 28 | - os: ubuntu-22.04 29 | target: x86_64-unknown-linux-gnu 30 | code-target: linux-x64 31 | - os: ubuntu-22.04 32 | target: aarch64-unknown-linux-gnu 33 | code-target: linux-arm64 34 | # - os: ubuntu-20.04 35 | # target: armv7-unknown-linux-gnueabihf 36 | # code-target: linux-armhf 37 | - os: macos-13 38 | target: x86_64-apple-darwin 39 | code-target: darwin-x64 40 | - os: macos-14 41 | target: aarch64-apple-darwin 42 | code-target: darwin-arm64 43 | 44 | env: 45 | LLM_LS_TARGET: ${{ matrix.target }} 46 | 47 | name: Publish in marketplace (VSCE) (${{ matrix.target }}) 48 | runs-on: ${{ matrix.os }} 49 | container: ${{ matrix.container }} 50 | 51 | steps: 52 | - name: Setup node 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: 20 56 | 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | with: 60 | fetch-depth: ${{ env.FETCH_DEPTH }} 61 | 62 | - name: Download lsp artifacts 63 | id: download-artifact-lsp 64 | uses: dawidd6/action-download-artifact@v3 65 | with: 66 | github_token: ${{secrets.GITHUB_TOKEN}} 67 | workflow: agent_engine_build.yml 68 | repo: smallcloudai/refact 69 | branch: ${{ inputs.lsp_branch }} 70 | path: ./assets 71 | name: dist-${{ matrix.target }} 72 | 73 | - name: Download chat artifacts 74 | id: download-artifact-chat 75 | uses: dawidd6/action-download-artifact@v9 76 | with: 77 | github_token: ${{secrets.GITHUB_TOKEN}} 78 | workflow: agent_gui_build.yml 79 | repo: smallcloudai/refact 80 | branch: ${{ inputs.chat_js_branch }} 81 | path: ./chat_package 82 | name: lts-refact-chat-js-.*\.tgz 83 | name_is_regexp: true 84 | 85 | - name: Prepare chat package 86 | shell: bash 87 | run: | 88 | mkdir -p ./chat_package_fixed 89 | find ./chat_package -name "*.tgz" -type f -exec cp {} ./chat_package_fixed/ \; 90 | ls -la ./chat_package_fixed 91 | 92 | - name: Install VSCE 93 | shell: bash 94 | run: | 95 | npm install ./chat_package_fixed/*.tgz 96 | npm install -g @vscode/vsce 97 | rm -rf ./chat_package ./chat_package_fixed 98 | 99 | - name: Package VSCE extension 100 | shell: bash 101 | run: | 102 | chmod +x ./assets/refact-lsp* 103 | if [[ ${{ inputs.stable_release }} != "true" ]]; then 104 | export PRERELEASE=--pre-release 105 | fi 106 | echo "PRERELEASE=${PRERELEASE}" 107 | vsce package --target ${{ matrix.code-target }} ${PRERELEASE} 108 | 109 | - name: Getting version 110 | id: get_version # Add this ID to reference the output 111 | shell: bash 112 | run: | 113 | VERSION=$(cat package.json | grep \"version\": | head -n1 | cut -d'"' -f4) 114 | echo "version=$VERSION" >> $GITHUB_OUTPUT # Store version in GitHub output 115 | 116 | - name: Release in GH 117 | uses: svenstaro/upload-release-action@v2 118 | with: 119 | repo_token: ${{ github.token }} 120 | file: ./*.vsix 121 | prerelease: ${{ !inputs.stable_release }} 122 | tag: v${{ steps.get_version.outputs.version }} # Use the version from previous step with 'v' prefix 123 | overwrite: true 124 | target_commit: ${{ github.sha }} 125 | file_glob: true 126 | 127 | - name: Publish VSCE extension 128 | shell: bash 129 | run: | 130 | if [[ ${{ inputs.stable_release }} != "true" ]]; then 131 | export PRERELEASE=--pre-release 132 | fi 133 | echo "PRERELEASE=${PRERELEASE}" 134 | vsce publish --skip-duplicate --pat ${{secrets.VSCE_PAT}} ${PRERELEASE} --packagePath *.vsix 135 | 136 | - name: Upload artifacts 137 | uses: actions/upload-artifact@v4 138 | with: 139 | name: vscode-plugin-${{ matrix.target }} 140 | path: ./*.vsix 141 | notify: 142 | needs: publish 143 | runs-on: ubuntu-latest 144 | steps: 145 | - name: Checkout repository 146 | uses: actions/checkout@v4 147 | with: 148 | fetch-depth: ${{ env.FETCH_DEPTH }} 149 | 150 | - name: Setup vars 151 | shell: bash 152 | id: setupvars 153 | run: | 154 | if [[ ${{ inputs.stable_release }} != "true" ]]; then 155 | echo "slack_notification_channel=prerelease" >> "$GITHUB_OUTPUT" 156 | else 157 | echo "slack_notification_channel=stable" >> "$GITHUB_OUTPUT" 158 | fi 159 | echo "plugin_version=$(cat package.json | jq -r '.version')" >> "$GITHUB_OUTPUT" 160 | 161 | - name: Post to a Slack channel 162 | id: slack 163 | uses: slackapi/slack-github-action@v1.26.0 164 | with: 165 | payload: | 166 | { 167 | "blocks": [ 168 | { 169 | "type": "header", 170 | "text": { 171 | "type": "plain_text", 172 | "text": "VSCode plugin ${{ steps.setupvars.outputs.plugin_version }} is released in ${{ steps.setupvars.outputs.slack_notification_channel }} channel", 173 | "emoji": true 174 | } 175 | }, 176 | { 177 | "type": "section", 178 | "text": { 179 | "type": "mrkdwn", 180 | "text": "by ${{ github.actor }}" 181 | } 182 | } 183 | ] 184 | } 185 | env: 186 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 187 | SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK 188 | 189 | - name: Notify to Discord 190 | run: | 191 | curl -X POST ${{ secrets.DISCORD_WEBHOOK_URL }} \ 192 | -H "Content-Type: application/json" \ 193 | -d '{"msg":"VSCode plugin ${{ steps.setupvars.outputs.plugin_version }} is released in ${{ steps.setupvars.outputs.slack_notification_channel }} channel"}' 194 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | src/test/* 4 | *.vsix 5 | out/* 6 | package-lock.json 7 | package.json 8 | images/.DS_Store 9 | .DS_Store 10 | assets/refact-lsp* 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "editor.formatOnSave": false 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": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Small Magellanic Cloud AI Ltd. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Refact 3 |

4 | 5 | --- 6 | 7 | [![Discord](https://img.shields.io/discord/1037660742440194089?logo=discord&label=Discord&link=https%3A%2F%2Fsmallcloud.ai%2Fdiscord)](https://smallcloud.ai/discord) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/refact_ai)](https://twitter.com/intent/follow?screen_name=refact_ai) 9 | ![License](https://img.shields.io/github/license/smallcloudai/refact-vscode) 10 | 11 | ## Refact.ai - Your Customizable Open-Source AI Software Engineering Agent 12 | 13 | Refact.ai is a free, **open-source** AI Agent that handles engineering tasks end-to-end. It deeply understands your codebases and integrates with your tools, databases, and browsers to automate complex, multi-step tasks. 14 | 15 | - Integrate AI Agent with the tools you already use, allowing it to complete tasks for you end-to-end. 16 | - Deploy Refact.ai on-premise and maintain **100% control over your codebase**. 17 | - Access State-of-the-Art Models (Claude 3.7 Sonnet, GPT-4o, o3-mini, etc.) 18 | - Bring your own key (BYOK) - Use your own API keys for external LLMs. 19 | - Stop switching between your IDE and chat—Refact.ai has an integrated chat right in your IDE. 20 | - Get free, unlimited, context-aware auto-completion. 21 | 22 | Suitable for both individual and enterprise use, supporting 25+ programming languages, including Python, JavaScript, Java, Rust, TypeScript, PHP, C++, C#, Go, and more. 23 | 24 | We can't say it better than our users: *'Refact.ai understands me better than my human coworkers. It's like working in perfect sync with an ideal partner.'* 25 | 26 | ## How does Refact.ai Agent empower developers? 27 | 28 | - **Unlimited accurate auto-completion with context awareness for free** - Powered by Qwen2.5-Coder-1.5B, Refact.ai uses Retrieval-Augmented Generation (RAG) to provide precise, context-aware code suggestions. By retrieving relevant snippets and project-specific insights, it ensures completions that align with your coding style and architecture.![auto-completion](https://github.com/user-attachments/assets/0ffab638-c807-4863-8d62-a663b1459805) 29 | - **Integrated in-IDE Chat** - By deeply understanding your codebases, Refact.ai provides relevant, intelligent answers to your questions—right inside your development environment.![refactor](https://github.com/user-attachments/assets/e7b2e779-85c5-46a0-99ad-ea909a69ddc7) 30 | - **Integrated with the tools** - Refact.ai goes beyond codebases, connecting with GitHub, GitLab, databases (PostgreSQL, MySQL), Pdb, Docker, and any shell command, so AI Agent can autonomously complete tasks—from code generation to deployment.![integrations](https://github.com/user-attachments/assets/daf8d0ee-0a54-4aa8-82e5-f968fded0c7a) 31 | - **State-of-the-Art Models** - Refact.ai supports **Claude 3.7 Sonnet, GPT-4o, and o3-mini**, allowing you to choose the best model for each task based on its complexity. 32 | - **Bring your own key (BYOK)** - Use your own API keys for external LLMs, ensuring flexibility and control over your AI infrastructure.![BYOK](https://github.com/user-attachments/assets/44e416f7-fb4d-4846-a1e0-1b7da32c2c75) 33 | - **Upload images**: Click the image button to add visual context to your chat. 34 | - **Use @-commands** to control what context to use to answer your question: 35 | - **@web** - define a webpage. 36 | - **@file** - upload and attach a single file. 37 | - **@definition** - find and attach a definition. 38 | - **@references** - locate all references and attach them. 39 | - **@tree** - display the workspace directory and files tree. 40 | - Create your own **custom system prompt** for a more personalized workflow.![@-commands](https://github.com/user-attachments/assets/28e1db76-3490-4195-a3e0-de30496239a9) 41 | 42 | ## Refact.ai Agent For Enterprise 43 | Deploying Refact.ai Agent is like adding a 24/7 engineer to your team—one that instantly understands your codebase, adapts to your workflows, accelerates development from day one, and becomes a shared knowledge base for your entire team. 44 | 45 | 1. **Refact.ai already understands your company's context:** AI Agent captures the unique structure, tools, and workflows of your organization, using your company's databases, documentation, and code architecture to deliver customized solutions. 46 | 2. **Gets smarter over time:** With each interaction and feedback, Refact.ai Agent adapts to your organization's needs, becoming more accurate and powerful. 47 | 3. **Organizes experience into the knowledge base:** Refact.ai Agent captures and shares knowledge from interactions with each team member. It not only streamlines workflows but also supports faster onboarding and smoother cross-functional collaboration across projects. 48 | 49 | ### Take full control of your AI Agent, tailored to your company: 50 | - **Deploy Refact.ai on-premise:** on your own servers or private cloud. Your data never leaves your control. Telemetry from the plugins goes to your server and nowhere else. You can verify what the code does, it's open source. 51 | - **Fine-tune a model on your codebase:** A fine-tuned code completion model will provide you with more relevant suggestions: it can memorize your coding style, the right way to use your internal APIs, and the tech stack you use. 52 | - **Priority Support:** Our engineers are always available to assist you at every stage, from setup to fine-tuning and beyond. 53 | 54 | **To get a 2-week free trial** for your team, just fill out the form on [our website](https://refact.ai/contact/?utm_source=vscode&utm_medium=marketplace&utm_campaign=enterprise). We'll reach out with more details! 55 | \ 56 |   57 | \ 58 |   59 | ## Which tasks can Refact.ai help me with? 60 | 61 | - **Generate code from natural language prompts (even if you make typos)** - Instantly turn ideas into functional code, accelerating development and eliminating the blank-screen problem. 62 | \ 63 |   64 | ![gen](https://github.com/user-attachments/assets/ef4acec7-4967-400a-900e-9d3382d05b1b) 65 | \ 66 |   67 | - **Refactor code** - Improve code quality, readability, and efficiency with automated refactoring that aligns with best practices. 68 | \ 69 |   70 | ![refactor](https://github.com/user-attachments/assets/2cae4467-f363-4033-8ecf-2854dcc74aaa) 71 | \ 72 |   73 | - **Explain code** - Quickly understand complex or unfamiliar code with AI-generated explanations, making collaboration and onboarding faster. 74 | \ 75 |   76 | ![explain](https://github.com/user-attachments/assets/bd43d9aa-15c9-49dc-9fa9-b5ab5c4ecdfe) 77 | \ 78 |   79 | - **Debug code** - Detect and fix errors faster with intelligent debugging, reducing time spent troubleshooting issues. 80 | \ 81 |   82 | ![ddbg](https://github.com/user-attachments/assets/45e917b5-f47b-4b84-b1f4-f4918c8a00c7) 83 | \ 84 |   85 | - **Generate unit tests** - Ensure code reliability by automatically creating comprehensive unit tests. 86 | \ 87 |   88 | ![unit](https://github.com/user-attachments/assets/5168ee57-e35b-4484-bf19-70cc0f3a6299) 89 | \ 90 |   91 | 92 | - **Code Review** - Get AI-assisted code reviews for cleaner, more secure, and more efficient code, addressing developers' top concern: accuracy. 93 | - **Create Documentation** - Automate documentation generation to keep knowledge accessible and up to date. 94 | - **Generate Docstrings** - Enhance maintainability with clear, structured documentation generated for functions and classes in seconds. 95 | 96 | ## Join Our Discord Community 97 | 98 | Connect with other developers in our [Discord community](https://www.smallcloud.ai/discord). Ask questions, share your opinion, propose new features. 99 | -------------------------------------------------------------------------------- /assets/codify.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-vscode/aac33fba974370cbeaa595acd0faa4afbe4e0215/assets/codify.woff -------------------------------------------------------------------------------- /assets/custom-theme.css: -------------------------------------------------------------------------------- 1 | :where(.vscode-light) { 2 | color-scheme: light; 3 | } 4 | 5 | :where(.vscode-dark) { 6 | color-scheme: dark; 7 | } 8 | 9 | .vscode-dark { 10 | color-scheme: dark; 11 | } 12 | 13 | .radix-themes { 14 | /** TODO: there are more theme-able variables we can use */ 15 | --default-font-family: var(--vscode-font-family); 16 | --code-font-family: var(--vscode-editor-font-family); 17 | --color-background: var(--vscode-sideBar-background); 18 | --color-panel: var(--vscode-panel-background); 19 | --color-surface: var(--vscode-panel-background); 20 | } 21 | 22 | .radix-themes > *:not(.rt-TooltipContent):not(.rt-TooltipText) { 23 | color: var(--vscode-sideBar-foreground); 24 | background-color: var(--color-background); 25 | } 26 | 27 | .radix-themes code { 28 | color: unset; 29 | background-color: unset; 30 | } 31 | 32 | :where([data-state-tabbed]) .radix-themes { 33 | --color-background: var(--vscode-editor-background); 34 | --color-panel: var(--vscode-input-background); 35 | color: var(--vscode-editor-foreground); 36 | } 37 | 38 | body { 39 | padding: 0; 40 | background-color: transparent; 41 | } 42 | 43 | .hljs { 44 | background: var(--vscode-textPreformat-background); 45 | } 46 | 47 | .hljs > code { 48 | background-color: transparent; 49 | } -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to the Project 3 | 4 | Thank you for your interest in contributing! This guide will walk you through the process of setting up the project locally so you can start contributing right away. 5 | 6 | ## Prerequisites 7 | 8 | Before you begin, ensure you have the following installed: 9 | 10 | - [Git](https://git-scm.com/) 11 | - [Node.js](https://nodejs.org/) (version 14.x or higher) 12 | - [npm](https://www.npmjs.com/) (Node Package Manager) 13 | 14 | ## Setup Instructions 15 | 16 | Follow these steps to set up the project: 17 | 18 | ### 1. Clone the `alpha` Branch of the `refact-chat-js` Repository 19 | 20 | Start by cloning the `alpha` branch of the `refact-chat-js` repository: 21 | 22 | ```bash 23 | git clone -b alpha https://github.com/your-username/refact-chat-js.git 24 | cd refact-chat-js 25 | ``` 26 | 27 | ### 2. Install Dependencies and Build the Project 28 | 29 | Once inside the `refact-chat-js` project folder, install the dependencies and build the project: 30 | 31 | ```bash 32 | npm ci # Clean install of dependencies 33 | npm run build # Build the project 34 | npm link # Create a global symlink to the project 35 | ``` 36 | 37 | ### 3. Clone the `dev` Branch of the `refact-vscode` Repository 38 | 39 | Next, in a new directory, clone the `dev` branch of the `refact-vscode` repository: 40 | 41 | ```bash 42 | git clone -b dev https://github.com/your-username/refact-vscode.git 43 | cd refact-vscode 44 | ``` 45 | 46 | ### 4. Link `refact-chat-js` to `refact-vscode` 47 | 48 | Link the `refact-chat-js` project to the `refact-vscode` project by running: 49 | 50 | ```bash 51 | npm link refact-chat-js 52 | ``` 53 | 54 | ### 5. Compile the `refact-vscode` Project 55 | 56 | Now, compile the `refact-vscode` project: 57 | 58 | ```bash 59 | npm run compile 60 | ``` 61 | 62 | ### 6. Open the IDE and Build the Project 63 | 64 | Open your IDE (e.g., Visual Studio Code) and load the `refact-vscode` project. Rebuild the project to apply the changes. 65 | 66 | ### 7. Update Settings for Chat Functionality 67 | 68 | To enable the latest chat features, go to your settings and change the following option: 69 | 70 | - **refactai.xDebug**: Set the value from `null` to `1`. 71 | 72 | ### 8. Test the Chat Functionality 73 | 74 | Once you've completed the setup, you can now test the chat functionality to ensure the latest features are working properly. 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare namespace JSX { 3 | interface Element {} 4 | } 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/add_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 103681_plus_icon + Rectangle 2 + Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /images/discussion-bubble.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-vscode/aac33fba974370cbeaa595acd0faa4afbe4e0215/images/logo-small.png -------------------------------------------------------------------------------- /images/refact-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-vscode/aac33fba974370cbeaa595acd0faa4afbe4e0215/images/refact-logo.png -------------------------------------------------------------------------------- /images/refact-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/refact-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-vscode/aac33fba974370cbeaa595acd0faa4afbe4e0215/images/refact-wide.png -------------------------------------------------------------------------------- /images/remove_line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-vscode/aac33fba974370cbeaa595acd0faa4afbe4e0215/logo-small.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codify", 3 | "displayName": "Refact – Open-Source AI Agent, Code Generator & Chat for JavaScript, Python, TypeScript, Java, PHP, Go, and more.", 4 | "description": "Refact.ai is a free, open-source AI Agent that adapts to your workflow. It tracks your cursor to provide instant assistance, connects with tools, databases and browsers, works with debuggers, and runs shell commands. As your AI copilot, it handles code generation, testing, review, and refactoring.", 5 | "publisher": "smallcloud", 6 | "icon": "logo-small.png", 7 | "galleryBanner": { 8 | "color": "#000000", 9 | "theme": "dark" 10 | }, 11 | "homepage": "https://refact.ai", 12 | "author": "Small Magellanic Cloud AI (https://refact.ai)", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/smallcloudai/refact-vscode" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/smallcloudai/refact-vscode/issues", 19 | "email": "support@smallcloud.tech" 20 | }, 21 | "version": "6.5.0", 22 | "dependencies": { 23 | "@types/marked": "^4.0.8", 24 | "@types/vscode": "^1.69.0", 25 | "diff": "^7.0.0", 26 | "difflib": "^0.2.4", 27 | "fetch-h2": "^3.0.2", 28 | "json5": "^2.2.3", 29 | "marked": "^4.0.8", 30 | "refact-chat-js": "^2.0.10-alpha.3", 31 | "uuid": "^9.0.1", 32 | "vscode-languageclient": "^7.0.0" 33 | }, 34 | "devDependencies": { 35 | "@types/diff": "^5.2.2", 36 | "@types/glob": "^7.2.0", 37 | "@types/mocha": "^9.1.1", 38 | "@types/node": "^20.4.2", 39 | "@types/uuid": "^9.0.4", 40 | "@types/vscode": "^1.69.0", 41 | "@typescript-eslint/eslint-plugin": "^5.30.0", 42 | "@typescript-eslint/parser": "^5.30.0", 43 | "@vscode/test-electron": "^2.1.5", 44 | "esbuild": "^0.16.3", 45 | "eslint": "^8.18.0", 46 | "glob": "^8.0.3", 47 | "mocha": "^10.0.0", 48 | "patch-package": "^8.0.0", 49 | "typescript": "^4.7.4" 50 | }, 51 | "engines": { 52 | "vscode": "^1.69.0" 53 | }, 54 | "categories": [ 55 | "Programming Languages", 56 | "Snippets", 57 | "Other", 58 | "Machine Learning", 59 | "Education", 60 | "Testing", 61 | "Data Science", 62 | "AI", 63 | "Chat" 64 | ], 65 | "keywords": [ 66 | "refact", 67 | "refact.ai", 68 | "refactoring", 69 | "copilot", 70 | "tabnine", 71 | "javascript", 72 | "python", 73 | "typescript", 74 | "php", 75 | "autocomplete", 76 | "ruby", 77 | "java", 78 | "go", 79 | "golang", 80 | "bash", 81 | "kotlin", 82 | "html", 83 | "scss", 84 | "vue", 85 | "react", 86 | "css", 87 | "ocaml", 88 | "perl", 89 | "rust", 90 | "julia", 91 | "lua", 92 | "haskell", 93 | "c", 94 | "cpp", 95 | "c++", 96 | "csharp", 97 | "c#", 98 | "react", 99 | "swift", 100 | "objective-c", 101 | "ai", 102 | "method completion", 103 | "intellicode", 104 | "intellisense", 105 | "snippets", 106 | "kite", 107 | "node", 108 | "node.js", 109 | "jupyter", 110 | "chat", 111 | "chatgpt", 112 | "code completion", 113 | "documentation", 114 | "refactor", 115 | "llm", 116 | "test", 117 | "security", 118 | "coding", 119 | "copilot", 120 | "cursor" 121 | ], 122 | "activationEvents": [ 123 | "onStartupFinished" 124 | ], 125 | "main": "./out/extension.js", 126 | "contributes": { 127 | "configuration": { 128 | "type": "object", 129 | "title": "Refact Assistant", 130 | "properties": { 131 | "refactai.addressURL": { 132 | "type": "string", 133 | "description": "For Enterprise, put there your company's server address. Your admin should have emailed that to you.\nFor self-hosted, use something like http://127.0.0.1:8008/\nFor inference in public cloud, use \"Refact\" or \"HF\".", 134 | "default": "", 135 | "order": 0 136 | }, 137 | "refactai.infurl": { 138 | "type": "string", 139 | "deprecationMessage": "The new field is called addressURL. It's about the same, but it has special values like \"HF\" and if it's empty, makes the plugin do nothing, which is better for Enterprise use case to prevent leaks.", 140 | "order": 0 141 | }, 142 | "refactai.apiKey": { 143 | "type": "string", 144 | "default": "", 145 | "description": "Secret API Key. It's used to authenticate your requests.", 146 | "order": 1 147 | }, 148 | "codify.apiKey": { 149 | "type": "string", 150 | "deprecationMessage": "Use refactai.apiKey instead.", 151 | "order": 1 152 | }, 153 | "refactai.insecureSSL": { 154 | "type": "boolean", 155 | "description": "Allow insecure server connections when using SSL, ignore certificate verification errors. Allows you to use self-signed certificates.", 156 | "default": false, 157 | "order": 2 158 | }, 159 | "refactai.submitChatWithShiftEnter": { 160 | "type": "boolean", 161 | "description": "Send chat messages with Shift+Enter instead of Enter.", 162 | "default": false, 163 | "order": 3 164 | }, 165 | "refactai.completionMaxTokens": { 166 | "type": "number", 167 | "markdownDescription": "Maximum number of tokens to generate for code completion. Leave 0 if not sure.", 168 | "default": 0, 169 | "order": 3 170 | }, 171 | "refactai.codeCompletionModel": { 172 | "type": "string", 173 | "description": "Which model to use, for example \"starcoder2/3b\". Leave blank if not sure.", 174 | "default": "", 175 | "order": 4 176 | }, 177 | "refactai.codeLens": { 178 | "type": "boolean", 179 | "description": "Enable inline annotations that provide chat actions directly within the code editor.", 180 | "default": true, 181 | "order": 5 182 | }, 183 | "refactai.codeLensDebug": { 184 | "type": "boolean", 185 | "description": "See what the parser sees, useful for debugging the parser.", 186 | "deprecationMessage": "debug only", 187 | "default": false, 188 | "order": 6 189 | }, 190 | "refactai.pauseCompletion": { 191 | "type": "boolean", 192 | "description": "Pause automatic code suggestions. Manual activation still works.", 193 | "default": false, 194 | "order": 7 195 | }, 196 | "refactai.ast": { 197 | "type": "boolean", 198 | "markdownDescription": "Enable context-aware code completion and chat, adds defintion() and references() functions for a chat model, and @definition, @references commands for you to add context manually.\n\nRAG works better with a larger context, available in the [Pro plan](https://refact.ai/pricing/?utm_source=vscode&utm_medium=settings&utm_campaign=plugin) and [for companies](https://refact.ai/enterprise/?utm_source=vscode&utm_medium=settings&utm_campaign=plugin).", 199 | "default": true, 200 | "order": 8 201 | }, 202 | "refactai.astFileLimit": { 203 | "type": "number", 204 | "description": "Limit the number of files for AST to process, to avoid memory issues. Increase if you have a large project and sufficient memory.", 205 | "default": 35000, 206 | "order": 9 207 | }, 208 | "refactai.vecdb": { 209 | "type": "boolean", 210 | "markdownDescription": "Enable vector database. This adds the ability for a chat model to perform a semantic search in your codebase, and the @workspace command for you to add context manually.\n\nRAG performs better with a larger context, available in the [Pro plan](https://refact.ai/pricing/?utm_source=vscode&utm_medium=settings&utm_campaign=plugin) and [for companies](https://refact.ai/enterprise/?utm_source=vscode&utm_medium=settings&utm_campaign=plugin).", 211 | "default": true, 212 | "order": 11 213 | }, 214 | "refactai.vecdbFileLimit": { 215 | "type": "number", 216 | "description": "Limit the number of files for VecDB to process. Increase if you have a lot of files.", 217 | "default": 15000, 218 | "order": 12 219 | }, 220 | "refactai.xDebug": { 221 | "type": "number|undefined", 222 | "description": "Set this to debug the Rust binary in console. If set, the plugin will not attempt to start its own, it will connect HTTP on the port 8001, LSP on the port 8002 instead.", 223 | "order": 13 224 | }, 225 | "refactai.xperimental": { 226 | "type": "boolean", 227 | "description": "Enable experimental features.", 228 | "default": false, 229 | "order": 14 230 | }, 231 | "refactai.activeGroup": { 232 | "type": ["object", "null"], 233 | "default": null, 234 | "description": "Active selected group in your Team Workspace. Modify via settings.json in your workspace", 235 | "scope": "machine-overridable", 236 | "included": false 237 | } 238 | } 239 | }, 240 | "properties": [], 241 | "commands": [ 242 | { 243 | "command": "refactaicmd.sendChatToSidebar", 244 | "title": "Open in Sidebar", 245 | "enablement": "refactaicmd.openSidebarButtonEnabled" 246 | }, 247 | { 248 | "command": "refactaicmd.closeInlineChat", 249 | "title": "Close" 250 | }, 251 | { 252 | "command": "refactaicmd.activateToolboxDeprecated", 253 | "title": "Activate Toolbox (deprecated)", 254 | "category": "Refact.ai" 255 | }, 256 | { 257 | "command": "refactaicmd.activateToolbox", 258 | "title": "Activate (opens chat)", 259 | "category": "Refact.ai" 260 | }, 261 | { 262 | "command": "refactaicmd.login", 263 | "title": "Login", 264 | "category": "Refact.ai" 265 | }, 266 | { 267 | "command": "refactaicmd.completionManual", 268 | "title": "Manual Completion Trigger", 269 | "category": "Refact.ai" 270 | }, 271 | { 272 | "command": "refactaicmd.callChat", 273 | "title": "Chat", 274 | "key": "F1", 275 | "category": "Refact.ai" 276 | }, 277 | { 278 | "command": "refactaicmd.openSettings", 279 | "title": "Settings Page", 280 | "category": "Refact.ai" 281 | }, 282 | { 283 | "command": "refactaicmd.openPromptCustomizationPage", 284 | "title": "Open Prompt Customization Page", 285 | "category": "Refact.ai" 286 | }, 287 | { 288 | "command": "refactaicmd.attachFile", 289 | "title": "Attach to chat", 290 | "category": "Refact.ai" 291 | } 292 | ], 293 | "menus": { 294 | "editor/context": [ 295 | { 296 | "group": "z_commands", 297 | "command": "refactaicmd.callChat" 298 | }, 299 | { 300 | "group": "9_cutcopypaste", 301 | "command": "refactaicmd.attachFile", 302 | "when": "refactai.chat_page == 'chat'" 303 | } 304 | ], 305 | "explorer/context": [ 306 | { 307 | "group": "5_cutcopypaste", 308 | "command": "refactaicmd.attachFile" 309 | } 310 | ] 311 | }, 312 | "keybindings": [ 313 | { 314 | "command": "refactaicmd.activateToolboxDeprecated", 315 | "key": "alt+t" 316 | }, 317 | { 318 | "command": "refactaicmd.completionManual", 319 | "key": "alt+space" 320 | }, 321 | { 322 | "command": "refactaicmd.esc", 323 | "key": "escape", 324 | "when": "refactcx.runEsc" 325 | }, 326 | { 327 | "command": "refactaicmd.tab", 328 | "key": "tab", 329 | "when": "refactcx.runTab" 330 | }, 331 | { 332 | "command": "refactaicmd.callChat", 333 | "key": "F1" 334 | } 335 | ], 336 | "viewsContainers": { 337 | "activitybar": [ 338 | { 339 | "id": "refact-toolbox-pane", 340 | "title": "Refact", 341 | "icon": "$(codify-logo)" 342 | } 343 | ] 344 | }, 345 | "views": { 346 | "refact-toolbox-pane": [ 347 | { 348 | "type": "webview", 349 | "id": "refactai-toolbox", 350 | "name": "" 351 | } 352 | ] 353 | }, 354 | "icons": { 355 | "codify-logo": { 356 | "description": "codify logo", 357 | "default": { 358 | "fontPath": "./assets/codify.woff", 359 | "fontCharacter": "\\e899" 360 | } 361 | }, 362 | "codify-bookmark-unchecked": { 363 | "description": "codify bookmark unchecked", 364 | "default": { 365 | "fontPath": "./assets/codify.woff", 366 | "fontCharacter": "\\e801" 367 | } 368 | }, 369 | "codify-bookmark-checked": { 370 | "description": "codify bookmark checked", 371 | "default": { 372 | "fontPath": "./assets/codify.woff", 373 | "fontCharacter": "\\e800" 374 | } 375 | }, 376 | "codify-like": { 377 | "description": "codify like", 378 | "default": { 379 | "fontPath": "./assets/codify.woff", 380 | "fontCharacter": "\\e808" 381 | } 382 | }, 383 | "codify-life-checked": { 384 | "description": "codify like checked", 385 | "default": { 386 | "fontPath": "./assets/codify.woff", 387 | "fontCharacter": "\\e807" 388 | } 389 | }, 390 | "codify-coin": { 391 | "description": "codify coin", 392 | "default": { 393 | "fontPath": "./assets/codify.woff", 394 | "fontCharacter": "\\e802" 395 | } 396 | }, 397 | "codify-chat": { 398 | "description": "codify chat", 399 | "default": { 400 | "fontPath": "./assets/codify.woff", 401 | "fontCharacter": "\\e804" 402 | } 403 | }, 404 | "codify-reload": { 405 | "description": "codify reload", 406 | "default": { 407 | "fontPath": "./assets/codify.woff", 408 | "fontCharacter": "\\e809" 409 | } 410 | }, 411 | "codify-settings": { 412 | "description": "codify settings", 413 | "default": { 414 | "fontPath": "./assets/codify.woff", 415 | "fontCharacter": "\\e806" 416 | } 417 | }, 418 | "codify-link": { 419 | "description": "codify link", 420 | "default": { 421 | "fontPath": "./assets/codify.woff", 422 | "fontCharacter": "\\e80A" 423 | } 424 | }, 425 | "codify-send": { 426 | "description": "codify send", 427 | "default": { 428 | "fontPath": "./assets/codify.woff", 429 | "fontCharacter": "\\e803" 430 | } 431 | }, 432 | "codify-logout": { 433 | "description": "codify logout", 434 | "default": { 435 | "fontPath": "./assets/codify.woff", 436 | "fontCharacter": "\\e812" 437 | } 438 | }, 439 | "refact-icon-privacy": { 440 | "description": "refacticon privacy", 441 | "default": { 442 | "fontPath": "./assets/codify.woff", 443 | "fontCharacter": "\\e811" 444 | } 445 | } 446 | } 447 | }, 448 | "scripts": { 449 | "vscode:prepublish": "npm run compile", 450 | "compile": "tsc -p ./", 451 | "watch": "tsc -watch -p ./", 452 | "pretest": "npm run compile && npm run lint", 453 | "lint": "eslint src --ext ts", 454 | "test": "node ./out/test/runTest.js", 455 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node", 456 | "esbuild": "npm run esbuild-base -- --sourcemap", 457 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch" 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/codeLens.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import * as estate from "./estate"; 5 | import * as fetchH2 from 'fetch-h2'; 6 | import * as fetchAPI from "./fetchAPI"; 7 | import { 8 | type ChatMessages, 9 | type ChatMessage, 10 | type ToolUse, 11 | setInputValue, 12 | isUserMessage, 13 | UserMessage, 14 | } from "refact-chat-js/dist/events"; 15 | 16 | 17 | class ExperimentalLens extends vscode.CodeLens { 18 | public constructor( 19 | range: vscode.Range, 20 | msg: string, 21 | arg0: string, 22 | arg1: string, 23 | ) { 24 | super(range, { 25 | title: msg, 26 | command: 'refactaicmd.codeLensClicked', 27 | arguments: [arg0, arg1, range] 28 | }); 29 | } 30 | } 31 | 32 | export let custom_code_lens: { [key: string]: any } | null = null; 33 | export class LensProvider implements vscode.CodeLensProvider 34 | { 35 | public notifyCodeLensesChanged: vscode.EventEmitter; 36 | public onDidChangeCodeLenses?: vscode.Event; 37 | 38 | constructor() 39 | { 40 | this.notifyCodeLensesChanged = new vscode.EventEmitter(); 41 | this.onDidChangeCodeLenses = this.notifyCodeLensesChanged.event; 42 | } 43 | 44 | async provideCodeLenses( 45 | document: vscode.TextDocument, 46 | ): Promise 47 | { 48 | const codeLensIsEnabled = vscode.workspace.getConfiguration("refactai").get("codeLens") ?? true; 49 | if (!codeLensIsEnabled) { 50 | return []; 51 | } 52 | const debug = vscode.workspace.getConfiguration("refactai").get("codeLensDebug") ?? false; 53 | let state = estate.state_of_document(document); 54 | if (!state) { 55 | return []; 56 | } 57 | 58 | let customization = await fetchAPI.get_prompt_customization(); 59 | 60 | const url = fetchAPI.rust_url("/v1/code-lens"); 61 | const request = new fetchH2.Request(url, { 62 | method: "POST", 63 | headers: { 64 | "Content-Type": "application/json", 65 | }, 66 | body: JSON.stringify({ 67 | uri: document.uri.toString(), 68 | debug: debug, 69 | }), 70 | }); 71 | 72 | const response = await fetchH2.fetch(request); 73 | let lenses: vscode.CodeLens[] = []; 74 | if (response.status !== 200) { 75 | console.log([`${url} http status`, response.status]); 76 | 77 | } else if ("code_lens" in customization) { 78 | custom_code_lens = customization["code_lens"] as { [key: string]: any }; 79 | const this_file_lens = await response.json(); 80 | if ("detail" in this_file_lens) { 81 | console.log(["/v1/code-lens error", this_file_lens["detail"]]); 82 | } 83 | if ("code_lens" in this_file_lens) { 84 | for (let i = this_file_lens["code_lens"].length - 1; i >= 0; i--) { 85 | let item = this_file_lens["code_lens"][i]; 86 | let range = new vscode.Range(item["line1"] - 1, 0, item["line2"] - 1, 0); 87 | if (item["spath"] !== "") { 88 | for (const [key, lensdict] of Object.entries(custom_code_lens)) { 89 | lenses.push(new ExperimentalLens(range, lensdict["label"], `CUSTOMLENS:${key}`, item["spath"])); 90 | } 91 | } else if (item["debug_string"] !== "") { 92 | lenses.push(new ExperimentalLens(range, item["debug_string"], "debug", "")); 93 | } else { 94 | console.log(["/v1/code-lens error", "no spath or debug_string"]); 95 | } 96 | } 97 | } 98 | } 99 | 100 | if (state.diff_lens_pos < document.lineCount) { 101 | let range = new vscode.Range(state.diff_lens_pos, 0, state.diff_lens_pos, 0); 102 | lenses.push(new ExperimentalLens(range, "👍 Approve (Tab)", "APPROVE", "")); 103 | lenses.push(new ExperimentalLens(range, "👎 Reject (Esc)", "REJECT", "")); 104 | // lenses.push(new ExperimentalLens(range, "↻ Rerun \"" + estate.global_intent + "\" (F1)", "RERUN")); // 🔃 105 | } 106 | 107 | state.completion_reset_on_cursor_movement = false; 108 | return lenses; 109 | } 110 | } 111 | 112 | const sendCodeLensToChat = ( 113 | messages: ChatMessage[], 114 | relative_path: string, 115 | text: string, 116 | auto_submit: boolean = false 117 | ) => { 118 | if (!global?.side_panel?._view || !messages) { 119 | return; 120 | } 121 | 122 | const firstMessage = messages[0]; 123 | const isOnlyOneUserMessage = messages.length === 1 && isUserMessage(firstMessage); 124 | const cursor = vscode.window.activeTextEditor?.selection.active.line ?? null; 125 | 126 | if (!isOnlyOneUserMessage) { 127 | const formattedMessages = formatMultipleMessagesForCodeLens(messages, relative_path, cursor, text); 128 | postMessageToWebview({ 129 | messages: formattedMessages, send_immediately: auto_submit 130 | }); 131 | return; 132 | } 133 | 134 | const messageBlock = createMessageBlock(firstMessage, relative_path, cursor, text); 135 | postMessageToWebview({ 136 | value: messageBlock, send_immediately: auto_submit 137 | }); 138 | }; 139 | 140 | const postMessageToWebview = ({messages, value, send_immediately}: {messages?: ChatMessage[], value?: string, send_immediately: boolean}) => { 141 | if (!global.side_panel?._view) { 142 | return; 143 | }; 144 | 145 | const eventMessage = setInputValue({ 146 | messages, 147 | value, 148 | send_immediately 149 | }); 150 | global.side_panel._view.webview.postMessage(eventMessage); 151 | }; 152 | 153 | const replaceVariablesInText = ( 154 | text: string, 155 | relative_path: string, 156 | cursor: number | null, 157 | code_selection: string 158 | ) => { 159 | return text 160 | .replace("%CURRENT_FILE%", relative_path) 161 | .replace('%CURSOR_LINE%', cursor ? (cursor + 1).toString() : '') 162 | .replace("%CODE_SELECTION%", code_selection) 163 | .replace("%PROMPT_EXPLORATION_TOOLS%", ''); 164 | }; 165 | 166 | const createMessageBlock = ( 167 | message: UserMessage, 168 | relative_path: string, 169 | cursor: number | null, 170 | text: string 171 | ) => { 172 | if (typeof message.content === 'string') { 173 | return replaceVariablesInText(message.content, relative_path, cursor, text); 174 | } else { 175 | return message.content.map(content => { 176 | if (('type' in content) && content.type === 'text') { 177 | return replaceVariablesInText(content.text, relative_path, cursor, text); 178 | } 179 | }).join("\n"); 180 | } 181 | }; 182 | 183 | const formatMultipleMessagesForCodeLens = ( 184 | messages: ChatMessage[], 185 | relative_path: string, 186 | cursor: number | null, 187 | text: string 188 | ) => { 189 | return messages.map(message => { 190 | if (isUserMessage(message)) { 191 | if (typeof message.content === 'string') { 192 | return { 193 | ...message, 194 | content: replaceVariablesInText(message.content, relative_path, cursor, text) 195 | }; 196 | } 197 | } 198 | return message; 199 | }); 200 | }; 201 | 202 | export async function code_lens_execute(code_lens: string, range: any) { 203 | if (!global) { return; } 204 | if (global.is_chat_streaming) { return; } 205 | global.is_chat_streaming = true; 206 | if (custom_code_lens) { 207 | const auto_submit = custom_code_lens[code_lens]["auto_submit"]; 208 | const new_tab = custom_code_lens[code_lens]["new_tab"]; 209 | let messages: ChatMessage[] = custom_code_lens[code_lens]["messages"]; 210 | 211 | const start_of_line = new vscode.Position(range.start.line, 0); 212 | const end_of_line = new vscode.Position(range.end.line + 1, 0); 213 | const block_range = new vscode.Range(start_of_line, end_of_line); 214 | 215 | const file_path = vscode.window.activeTextEditor?.document.fileName || ""; 216 | const workspaceFolders = vscode.workspace.workspaceFolders; 217 | let relative_path: string = ""; 218 | 219 | if (workspaceFolders) { 220 | const workspacePath = workspaceFolders[0].uri.fsPath; 221 | relative_path = path.relative(workspacePath, file_path); 222 | } 223 | 224 | let text = vscode.window.activeTextEditor!.document.getText(block_range); 225 | 226 | if (messages.length === 0) { 227 | global.is_chat_streaming = false; 228 | const editor = vscode.window.activeTextEditor; 229 | if (!editor) { 230 | return; 231 | } 232 | editor.selection = new vscode.Selection(start_of_line, end_of_line); 233 | editor.revealRange(block_range); 234 | vscode.commands.executeCommand('refactaicmd.callChat', ''); 235 | return; 236 | } 237 | 238 | if (global && global.side_panel && global.side_panel._view && global.side_panel._view.visible) { 239 | const current_page = global.side_panel.context.globalState.get("chat_page"); 240 | if (typeof current_page === "string" && current_page !== '"chat"' || new_tab) { 241 | vscode.commands.executeCommand('refactaicmd.callChat', ''); 242 | } 243 | sendCodeLensToChat(messages, relative_path, text, auto_submit); 244 | if (!auto_submit) { 245 | global.is_chat_streaming = false; 246 | } 247 | } else { 248 | vscode.commands.executeCommand('refactaicmd.callChat', ''); 249 | sendCodeLensToChat(messages, relative_path, text, auto_submit); 250 | if (!auto_submit) { 251 | global.is_chat_streaming = false; 252 | } 253 | } 254 | } 255 | } 256 | 257 | 258 | export var global_provider: LensProvider | null = null; 259 | 260 | 261 | export function save_provider(provider: LensProvider) 262 | { 263 | global_provider = provider; 264 | } 265 | 266 | 267 | export function quick_refresh() 268 | { 269 | if (global_provider) { 270 | console.log(`[DEBUG]: refreshing code lens!`); 271 | global_provider.notifyCodeLensesChanged.fire(); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/completionProvider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as estate from "./estate"; 4 | import * as fetchAPI from "./fetchAPI"; 5 | import * as fetchH2 from 'fetch-h2'; 6 | import { FimDebugData, fim } from 'refact-chat-js/dist/events'; 7 | 8 | 9 | export class MyInlineCompletionProvider implements vscode.InlineCompletionItemProvider 10 | { 11 | async provideInlineCompletionItems( 12 | document: vscode.TextDocument, 13 | position: vscode.Position, 14 | context: vscode.InlineCompletionContext, 15 | cancelToken: vscode.CancellationToken 16 | ) 17 | { 18 | if(document.uri.scheme === "comment") { 19 | return []; 20 | } 21 | 22 | let state = estate.state_of_document(document); 23 | if (state) { 24 | if (state.get_mode() !== estate.Mode.Normal && state.get_mode() !== estate.Mode.Highlight) { 25 | return []; 26 | } 27 | } 28 | let pause_completion = vscode.workspace.getConfiguration().get('refactai.pauseCompletion'); 29 | if (pause_completion && context.triggerKind === vscode.InlineCompletionTriggerKind.Automatic) { 30 | return []; 31 | } 32 | 33 | let file_name = document.fileName; // to test canonical path in rust add .toUpperCase(); 34 | let current_line = document.lineAt(position.line); 35 | let left_of_cursor = current_line.text.substring(0, position.character); 36 | let right_of_cursor = current_line.text.substring(position.character); 37 | let right_of_cursor_has_only_special_chars = Boolean(right_of_cursor.match(/^[:\s\t\n\r(){},."'\];]*$/)); 38 | if (!right_of_cursor_has_only_special_chars) { 39 | return []; 40 | } 41 | let multiline = left_of_cursor.replace(/\s/g, "").length === 0; 42 | let whole_doc = document.getText(); 43 | if (whole_doc.length > 180*1024) { // Too big (180k is ~0.2% of all files on our dataset) everything becomes heavy: network traffic, cache, cpu 44 | return []; 45 | } 46 | 47 | // let debounce_if_not_cached = context.triggerKind === vscode.InlineCompletionTriggerKind.Automatic; 48 | let called_manually = context.triggerKind === vscode.InlineCompletionTriggerKind.Invoke; 49 | 50 | let completion = ""; 51 | let corrected_cursor_character = 0; 52 | if (!multiline) { 53 | // VS Code uses UCS-2 or some older encoding internally, so emojis, Chinese characters, are more than one char 54 | // according to string.length 55 | let replace_emoji_with_one_char = left_of_cursor.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, " "); 56 | corrected_cursor_character = position.character; 57 | corrected_cursor_character -= left_of_cursor.length - replace_emoji_with_one_char.length; 58 | } 59 | 60 | let this_completion_serial_number = 6000; 61 | [completion, this_completion_serial_number] = await this.cached_request( 62 | cancelToken, 63 | file_name, 64 | whole_doc, 65 | position.line, 66 | corrected_cursor_character, 67 | multiline, 68 | called_manually, 69 | ); 70 | 71 | let command = { 72 | command: "refactaicmd.inlineAccepted", 73 | title: "inlineAccepted", 74 | arguments: [this_completion_serial_number], 75 | }; 76 | 77 | let replace_range0 = new vscode.Position(position.line, position.character); 78 | let replace_range1 = new vscode.Position(position.line, current_line.text.length); 79 | if (multiline) { 80 | replace_range0 = new vscode.Position(position.line, 0); 81 | } 82 | console.log([ 83 | "completion", completion, 84 | "replace_range0", replace_range0.line, replace_range0.character, 85 | "replace_range1", replace_range1.line, replace_range1.character, 86 | ]); 87 | let completionItem = new vscode.InlineCompletionItem( 88 | completion, 89 | new vscode.Range(replace_range0, replace_range1), 90 | command, 91 | ); 92 | return [completionItem]; 93 | } 94 | 95 | private called_manually_count: number = 0; 96 | 97 | async cached_request( 98 | cancelToken: vscode.CancellationToken, 99 | file_name: string, 100 | whole_doc: string, 101 | cursor_line: number, 102 | cursor_character: number, 103 | multiline: boolean, 104 | called_manually: boolean 105 | ): Promise<[string, number]> 106 | { 107 | if (!global.have_caps) { 108 | await global.rust_binary_blob?.read_caps(); 109 | } 110 | if (cancelToken.isCancellationRequested) { 111 | return ["", -1]; 112 | } 113 | 114 | let request = new fetchAPI.PendingRequest(undefined, cancelToken); 115 | let max_tokens_ = vscode.workspace.getConfiguration().get('refactai.completionMaxTokens'); 116 | let max_tokens: number; 117 | if (!max_tokens_ || typeof max_tokens_ !== "number") { 118 | max_tokens = 50; 119 | } else { 120 | max_tokens = max_tokens_; 121 | } 122 | 123 | let sources: { [key: string]: string } = {}; 124 | sources[file_name] = whole_doc; 125 | 126 | let t0 = Date.now(); 127 | let promise: any; 128 | let no_cache = called_manually; 129 | 130 | if (called_manually) { 131 | this.called_manually_count++; 132 | } else { 133 | this.called_manually_count = 0; 134 | } 135 | 136 | let temperature = 0.2; 137 | if (this.called_manually_count > 1) { 138 | temperature = 0.6; 139 | } 140 | 141 | promise = fetchAPI.fetch_code_completion( 142 | cancelToken, 143 | sources, 144 | multiline, 145 | file_name, 146 | cursor_line, 147 | cursor_character, 148 | max_tokens, 149 | no_cache, 150 | temperature, 151 | ); 152 | request.supply_stream(promise, "completion", ""); 153 | let json: any; 154 | json = await request.apiPromise; 155 | if (json === undefined) { 156 | return ["", -1]; 157 | } 158 | let t1 = Date.now(); 159 | let ms_int = Math.round(t1 - t0); 160 | console.log([`API request ${ms_int}ms`]); 161 | 162 | let completion = json["choices"][0]["code_completion"]; 163 | if (completion === undefined || completion === "" || completion === "\n") { 164 | console.log(["completion is empty", completion]); 165 | return ["", -1]; 166 | } 167 | let de_facto_model = json["model"]; 168 | let serial_number = json["snippet_telemetry_id"]; 169 | this.maybeSendFIMData(json); 170 | global.status_bar.completion_model_worked(de_facto_model); 171 | return [completion, serial_number]; 172 | } 173 | 174 | maybeSendFIMData(data: FimDebugData | null) { 175 | if(data === null) { return; } 176 | global.fim_data_cache = data; 177 | global.side_panel?._view?.webview.postMessage(fim.receive(data)); 178 | } 179 | } 180 | 181 | export function _extract_extension(feed: estate.ApiFields) 182 | { 183 | let filename_ext = feed.cursor_file.split("."); 184 | let ext = "None"; 185 | if (filename_ext.length > 1) { 186 | let try_this = filename_ext[filename_ext.length - 1]; 187 | if (try_this.length <= 4) { 188 | ext = try_this; 189 | } 190 | } 191 | return ext; 192 | } 193 | 194 | 195 | export async function inline_accepted(serial_number: number) 196 | { 197 | let url = fetchAPI.rust_url("/v1/snippet-accepted"); 198 | if (!url) { 199 | console.log(["Failed to get url for /v1/snippet-accepted"]); 200 | } 201 | const post = JSON.stringify({ 202 | "snippet_telemetry_id": serial_number 203 | }); 204 | const headers = { 205 | "Content-Type": "application/json", 206 | // "Authorization": `Bearer ${apiKey}`, 207 | }; 208 | let req = new fetchH2.Request(url, { 209 | method: "POST", 210 | headers: headers, 211 | body: post, 212 | redirect: "follow", 213 | cache: "no-cache", 214 | referrer: "no-referrer" 215 | }); 216 | 217 | try { 218 | await fetchH2.fetch(req); 219 | } catch (error) { 220 | console.log("failed to post to /v1/snippet-accepted"); 221 | } 222 | } 223 | 224 | 225 | export function inline_rejected(reason: string) 226 | { 227 | // console.log(["inline_rejected", reason]); 228 | } 229 | 230 | 231 | export function on_cursor_moved() 232 | { 233 | setTimeout(() => { 234 | inline_rejected("moveaway"); 235 | }, 50); 236 | } 237 | 238 | 239 | export function on_text_edited() 240 | { 241 | setTimeout(() => { 242 | inline_rejected("moveaway"); 243 | }, 50); 244 | } 245 | 246 | 247 | export function on_esc_pressed() 248 | { 249 | inline_rejected("esc"); 250 | } 251 | -------------------------------------------------------------------------------- /src/crlf.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | 3 | 4 | export function cleanup_cr_lf( 5 | text: string, 6 | cursors: number[] 7 | ): [string, number[], number[]] 8 | { 9 | let text_cleaned: string = text.replace(/\r\n/g, "\n"); 10 | let cursor_cleaned_cr: number[] = []; 11 | let cursor_transmit: number[] = []; 12 | for (let i = 0; i < cursors.length; i++) { 13 | let cursor = cursors[i]; 14 | let text_left = text.substring(0, cursor); 15 | 16 | let text_left_cleaned = text_left.replace(/\r\n/g, "\n"); 17 | let cleaned_cr = cursor - (text_left.length - text_left_cleaned.length); 18 | cursor_cleaned_cr.push(cleaned_cr); 19 | 20 | let replace_emoji_with_one_char = text_left_cleaned.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, " "); 21 | cursor_transmit.push(cleaned_cr - (text_left_cleaned.length - replace_emoji_with_one_char.length)); 22 | } 23 | return [text_cleaned, cursor_cleaned_cr, cursor_transmit]; 24 | } 25 | 26 | export function add_back_cr_lf( 27 | text: string, 28 | cursors: number[] 29 | ): number[] 30 | { 31 | for (let i = 0; i < text.length; i++) { 32 | if (text[i] === "\r") { 33 | cursors = cursors.map((cursor) => { 34 | if (cursor > i) { 35 | return cursor + 1; 36 | } 37 | return cursor; 38 | }); 39 | } 40 | } 41 | return cursors; 42 | } 43 | 44 | 45 | export function simple_cleanup_cr_lf( 46 | text: string 47 | ): string { 48 | let text_cleaned: string = text.replace(/\r\n/g, "\n"); 49 | return text_cleaned; 50 | } 51 | -------------------------------------------------------------------------------- /src/estate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as interactiveDiff from "./interactiveDiff"; 4 | import * as codeLens from "./codeLens"; 5 | import * as completionProvider from "./completionProvider"; 6 | import * as fetchAPI from "./fetchAPI"; 7 | 8 | export let global_intent: string = "Fix"; 9 | 10 | 11 | export enum Mode { 12 | Normal, 13 | Highlight, 14 | Diff, 15 | DiffWait, 16 | Dispose, 17 | }; 18 | 19 | 20 | export class ApiFields { 21 | public scope: string = ""; 22 | public positive: boolean = false; 23 | public url: string = ""; 24 | public model: string = ""; 25 | public function: string = ""; 26 | public intent: string = ""; 27 | public sources: { [key: string]: string } = {}; 28 | public cursor_file: string = ""; 29 | public cursor_pos0: number = 0; 30 | public cursor_pos1: number = 0; 31 | public de_facto_model: string = ""; 32 | public results: { [key: string]: string } = {}; // filled for diff 33 | public grey_text_explicitly: string = ""; // filled for completion 34 | public grey_text_edited: string = ""; // user changed something within completion 35 | public messages: [string, string][] = []; // filled for chat 36 | public ts_req: number = 0; 37 | public ts_presented: number = 0; 38 | public ts_reacted: number = 0; 39 | public serial_number: number = 0; 40 | public accepted: boolean = false; 41 | public rejected_reason: string = ""; 42 | }; 43 | 44 | 45 | export class StateOfEditor { 46 | public editor: vscode.TextEditor; 47 | 48 | _mode: Mode = Mode.Normal; 49 | public get_mode(): Mode { 50 | return this._mode; 51 | } 52 | public last_used_ts: number = 0; 53 | public fn: string = ""; 54 | 55 | public highlight_json_backup: any = undefined; 56 | public highlight_function: string = ""; 57 | public highlight_model: string = ""; 58 | public highlight_thirdparty: boolean = false; 59 | public highlights: any = []; 60 | public sensitive_ranges: vscode.DecorationOptions[] = []; 61 | 62 | public diff_changing_doc: boolean = false; 63 | public diffDecos: any = []; 64 | public diffDeletedLines: number[] = []; 65 | public diffAddedLines: number[] = []; 66 | 67 | public diff_lens_pos: number = Number.MAX_SAFE_INTEGER; 68 | public completion_lens_pos: number = Number.MAX_SAFE_INTEGER; 69 | public completion_longthink: number = 0; 70 | public completion_reset_on_cursor_movement: boolean = false; 71 | 72 | public showing_diff_modif_doc: string | undefined; 73 | public showing_diff_move_cursor: boolean = false; 74 | public showing_diff_for_range: vscode.Range | undefined = undefined; 75 | public showing_diff_for_function: string | undefined = undefined; 76 | public showing_diff_for_model: string | undefined = undefined; 77 | public showing_diff_thirdparty: boolean = true; 78 | public diff_load_animation_head: number = 0; 79 | public diff_load_animation_mid: string = ""; 80 | 81 | public cursor_move_event: vscode.Disposable|undefined = undefined; 82 | public text_edited_event: vscode.Disposable|undefined = undefined; 83 | 84 | // public data_feedback_candidate: ApiFields|undefined = undefined; 85 | 86 | constructor(editor: vscode.TextEditor) 87 | { 88 | this.editor = editor; 89 | } 90 | 91 | public cache_clear() 92 | { 93 | // call on text edited, intent change 94 | // this.area2cache.clear(); 95 | this.highlight_json_backup = undefined; 96 | this.sensitive_ranges.length = 0; 97 | this.highlights.length = 0; 98 | } 99 | }; 100 | 101 | 102 | let editor2state = new Map(); 103 | 104 | 105 | export function state_of_editor(editor: vscode.TextEditor|undefined, reqfrom: string): StateOfEditor | undefined 106 | { 107 | if (!editor) { 108 | return undefined; 109 | } 110 | if (editor2state.size > 3) { 111 | let oldest_ts = Number.MAX_SAFE_INTEGER; 112 | let oldest_state: StateOfEditor | undefined = undefined; 113 | for (let [_, state] of editor2state) { 114 | if (state.last_used_ts < oldest_ts) { 115 | oldest_ts = state.last_used_ts; 116 | oldest_state = state; 117 | } 118 | } 119 | if (!oldest_state) { 120 | throw new Error("Internal error"); 121 | } 122 | // console.log(["forget state of", oldest_state.editor.document.fileName, oldest_state.fn]); 123 | switch_mode(oldest_state, Mode.Dispose); 124 | editor2state.delete(oldest_state.editor); 125 | } 126 | let state = editor2state.get(editor); 127 | if (!state) { 128 | let current_editor = vscode.window.activeTextEditor; 129 | for (const [other_editor, other_state] of editor2state) { 130 | if (other_editor.document === editor.document) { 131 | if (other_state.editor === current_editor) { 132 | console.log([reqfrom, "state return other AKA current", other_state.fn]); 133 | return other_state; 134 | } 135 | if (editor === current_editor) { 136 | console.log([reqfrom, "state delete/add AKA new is current", other_state.fn]); 137 | editor2state.delete(other_editor); 138 | editor2state.set(editor, other_state); 139 | state = other_state; 140 | state.editor = editor; 141 | break; 142 | } 143 | } 144 | } 145 | } else { 146 | // console.log([reqfrom, "found", state.fn]); 147 | } 148 | if (!state) { 149 | state = new StateOfEditor(editor); 150 | state.last_used_ts = Date.now(); 151 | state.fn = editor.document.fileName; 152 | editor2state.set(editor, state); 153 | console.log([reqfrom, "state create new", state.fn]); 154 | } 155 | state.last_used_ts = Date.now(); 156 | return state; 157 | } 158 | 159 | export function state_of_document(doc: vscode.TextDocument): StateOfEditor | undefined 160 | { 161 | let candidates_list = []; 162 | for (const [editor, state] of editor2state) { 163 | if (editor.document === doc) { 164 | candidates_list.push(state); 165 | } 166 | } 167 | if (candidates_list.length === 0) { 168 | return undefined; 169 | } 170 | if (candidates_list.length === 1) { 171 | return candidates_list[0]; 172 | } 173 | console.log(["multiple editors/states for the same document, taking the most recent...", doc.fileName]); 174 | let most_recent_ts = 0; 175 | let most_recent_state: StateOfEditor | undefined = undefined; 176 | for (let state of candidates_list) { 177 | if (state.last_used_ts > most_recent_ts) { 178 | most_recent_ts = state.last_used_ts; 179 | most_recent_state = state; 180 | } 181 | } 182 | return most_recent_state; 183 | } 184 | 185 | 186 | export async function switch_mode(state: StateOfEditor, new_mode: Mode) 187 | { 188 | let old_mode = state._mode; 189 | console.log(["switch mode", old_mode, new_mode]); 190 | state._mode = new_mode; 191 | 192 | if (old_mode === Mode.Diff) { 193 | await interactiveDiff.dislike_and_rollback(state.editor); 194 | vscode.commands.executeCommand('setContext', 'refactcx.runTab', false); 195 | vscode.commands.executeCommand('setContext', 'refactcx.runEsc', false); 196 | } else if (old_mode === Mode.Highlight) { 197 | // highlight.hl_clear(state.editor); 198 | } else if (old_mode === Mode.DiffWait) { 199 | // highlight.hl_clear(state.editor); 200 | } 201 | 202 | if (new_mode === Mode.Diff) { 203 | if (state.showing_diff_modif_doc !== undefined) { 204 | await interactiveDiff.present_diff_to_user(state.editor, state.showing_diff_modif_doc, state.showing_diff_move_cursor); 205 | state.showing_diff_move_cursor = false; 206 | vscode.commands.executeCommand('setContext', 'refactcx.runTab', true); 207 | vscode.commands.executeCommand('setContext', 'refactcx.runEsc', true); 208 | } else { 209 | console.log(["cannot enter diff state, no diff modif doc"]); 210 | } 211 | } 212 | if (new_mode === Mode.Highlight) { 213 | state.diff_lens_pos = Number.MAX_SAFE_INTEGER; 214 | codeLens.quick_refresh(); 215 | if (state.highlight_json_backup !== undefined) { 216 | // highlight.hl_show(state.editor, state.highlight_json_backup); 217 | } else { 218 | console.log(["cannot enter highlight state, no hl json"]); 219 | } 220 | } 221 | // if (new_mode === Mode.Normal) { 222 | // } 223 | if (new_mode !== Mode.Dispose) { 224 | keyboard_events_on(state.editor); 225 | } 226 | if (new_mode !== Mode.Normal) { 227 | vscode.commands.executeCommand('setContext', 'refactcx.runEsc', true); 228 | } else { 229 | // keyboard_events_off(state); 230 | } 231 | } 232 | 233 | 234 | export async function back_to_normal(state: StateOfEditor) 235 | { 236 | await switch_mode(state, Mode.Normal); 237 | } 238 | 239 | 240 | function info2sidebar(ev_editor: vscode.TextEditor|undefined) 241 | { 242 | // if(global.side_panel !== undefined) { 243 | // global.side_panel.editor_inform_how_many_lines_selected(ev_editor); 244 | // } 245 | } 246 | 247 | 248 | export function keyboard_events_on(editor: vscode.TextEditor) 249 | { 250 | let state = state_of_editor(editor, "keyb_on"); 251 | if (!state) { 252 | return; 253 | } 254 | if (state.cursor_move_event) { 255 | state.cursor_move_event.dispose(); 256 | } 257 | if (state.text_edited_event) { 258 | state.text_edited_event.dispose(); 259 | } 260 | 261 | state.cursor_move_event = vscode.window.onDidChangeTextEditorSelection(async (ev: vscode.TextEditorSelectionChangeEvent) => { 262 | completionProvider.on_cursor_moved(); 263 | let is_mouse = ev.kind === vscode.TextEditorSelectionChangeKind.Mouse; 264 | let ev_editor = ev.textEditor; 265 | let pos1 = ev_editor.selection.active; 266 | info2sidebar(ev_editor); 267 | if (!editor || editor !== ev_editor) { 268 | return; 269 | } 270 | await interactiveDiff.on_cursor_moved(editor, pos1, is_mouse); 271 | if (state && state.completion_reset_on_cursor_movement) { 272 | state.completion_lens_pos = Number.MAX_SAFE_INTEGER; 273 | state.completion_longthink = 0; 274 | codeLens.quick_refresh(); 275 | } 276 | }); 277 | state.text_edited_event = vscode.workspace.onDidChangeTextDocument((ev: vscode.TextDocumentChangeEvent) => { 278 | completionProvider.on_text_edited(); 279 | let doc = ev.document; 280 | let ev_doc = editor.document; 281 | if (doc !== ev_doc) { 282 | return; 283 | } 284 | on_text_edited(editor); 285 | }); 286 | info2sidebar(vscode.window.activeTextEditor); 287 | } 288 | 289 | 290 | function keyboard_events_off(state: StateOfEditor) 291 | { 292 | if (state.cursor_move_event !== undefined) { 293 | state.cursor_move_event.dispose(); 294 | state.cursor_move_event = undefined; 295 | } 296 | if (state.text_edited_event !== undefined) { 297 | state.text_edited_event.dispose(); 298 | state.text_edited_event = undefined; 299 | } 300 | } 301 | 302 | 303 | export function on_text_edited(editor: vscode.TextEditor) 304 | { 305 | let state = state_of_editor(editor, "text_edited"); 306 | if (!state) { 307 | return; 308 | } 309 | if (state.diff_changing_doc) { 310 | console.log(["text edited, do nothing"]); 311 | return; 312 | } 313 | if (state._mode === Mode.Diff || state._mode === Mode.DiffWait) { 314 | console.log(["text edited mode", state._mode, "hands off"]); 315 | interactiveDiff.hands_off_dont_remove_anything(editor); 316 | state.highlight_json_backup = undefined; 317 | state.diff_lens_pos = Number.MAX_SAFE_INTEGER; 318 | state.completion_lens_pos = Number.MAX_SAFE_INTEGER; 319 | // state.area2cache.clear(); 320 | switch_mode(state, Mode.Normal); 321 | } else if (state._mode === Mode.Highlight) { 322 | // highlight.hl_clear(editor); 323 | state.highlight_json_backup = undefined; 324 | // state.area2cache.clear(); 325 | switch_mode(state, Mode.Normal); 326 | } else if (state._mode === Mode.Normal) { 327 | // state.area2cache.clear(); 328 | state.highlight_json_backup = undefined; 329 | } 330 | } 331 | 332 | function on_change_active_editor(editor: vscode.TextEditor | undefined) 333 | { 334 | if (editor) { 335 | let state_stored = editor2state.get(editor); 336 | if (!state_stored) { 337 | let state = state_of_editor(editor, "change_active"); 338 | if (state) { 339 | // this does almost nothing, but the state will be there for inline completion to pick up 340 | switch_mode(state, Mode.Normal); 341 | } 342 | } 343 | fetchAPI.lsp_set_active_document(editor); 344 | } 345 | info2sidebar(editor); 346 | } 347 | 348 | export function estate_init() 349 | { 350 | let disposable9 = vscode.window.onDidChangeActiveTextEditor(on_change_active_editor); 351 | let current_editor = vscode.window.activeTextEditor; 352 | if (current_editor) { 353 | on_change_active_editor(current_editor); 354 | } 355 | return [disposable9]; 356 | } 357 | 358 | 359 | export function save_intent(intent: string) 360 | { 361 | if (global_intent !== intent) { 362 | global_intent = intent; 363 | for (const [editor, state] of editor2state) { 364 | // state.area2cache.clear(); 365 | state.highlight_json_backup = undefined; 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable @typescript-eslint/naming-convention */ 3 | import * as vscode from 'vscode'; 4 | import * as completionProvider from "./completionProvider"; 5 | import * as statusBar from "./statusBar"; 6 | import * as codeLens from "./codeLens"; 7 | import * as interactiveDiff from "./interactiveDiff"; 8 | import * as estate from "./estate"; 9 | import * as fetchAPI from "./fetchAPI"; 10 | import * as userLogin from "./userLogin"; 11 | import * as sidebar from "./sidebar"; 12 | import * as launchRust from "./launchRust"; 13 | import { RefactConsoleProvider } from './rconsoleProvider'; 14 | import { QuickActionProvider } from "./quickProvider"; 15 | 16 | import * as os from 'os'; 17 | import * as path from 'path'; 18 | import { Mode } from "./estate"; 19 | import { fileURLToPath } from 'url'; 20 | import { ChatTab } from './chatTab'; 21 | import { FimDebugData } from 'refact-chat-js/dist/events/index.js'; 22 | import { code_lens_execute } from './codeLens'; 23 | 24 | 25 | declare global { 26 | var rust_binary_blob: launchRust.RustBinaryBlob|undefined; 27 | var status_bar: statusBar.StatusBarMenu; 28 | var side_panel: sidebar.PanelWebview|undefined; 29 | // var streamlined_login_ticket: string; 30 | // var streamlined_login_countdown: number; 31 | // var user_logged_in: string; 32 | // var user_active_plan: string; 33 | // var user_metering_balance: number; 34 | // var api_key: string; 35 | var global_context: vscode.ExtensionContext; 36 | var enable_longthink_completion: boolean; 37 | var last_positive_result: number; 38 | var chat_models: string[]; 39 | var chat_default_model: string; 40 | var have_caps: boolean; 41 | // TODO: remove this. 42 | var open_chat_tabs: ChatTab[]; 43 | var comment_disposables: vscode.Disposable[]; 44 | var comment_file_uri: vscode.Uri|undefined; 45 | var is_chat_streaming: boolean | undefined; 46 | var open_chat_panels: Record; 47 | 48 | var toolbox_config: launchRust.ToolboxConfig | undefined; 49 | var toolbox_command_disposables: vscode.Disposable[]; 50 | 51 | var fim_data_cache: FimDebugData | undefined; 52 | } 53 | 54 | async function pressed_call_chat(n = 0) { 55 | let editor = vscode.window.activeTextEditor; 56 | if(global.side_panel && !global.side_panel._view) { 57 | 58 | await vscode.commands.executeCommand(sidebar.default.viewType + ".focus"); 59 | 60 | const delay = (n + 1) * 10; 61 | if(delay > 200) { return; } 62 | 63 | setTimeout(() => pressed_call_chat(n + 1), delay); 64 | return; 65 | } else if (global.side_panel && global.side_panel._view && !global.side_panel?._view?.visible) { 66 | global.side_panel._view.show(); 67 | } 68 | 69 | global.side_panel?.newChat(); 70 | } 71 | 72 | 73 | async function pressed_escape() 74 | { 75 | console.log(["pressed_escape"]); 76 | completionProvider.on_esc_pressed(); 77 | let editor = vscode.window.activeTextEditor; 78 | if (global.comment_disposables) { 79 | // let original_editor_uri = rconsoleProvider.refact_console_close(); 80 | let original_editor_uri = RefactConsoleProvider.close_all_consoles(); 81 | if (original_editor_uri !== undefined) { 82 | let original_editor = vscode.window.visibleTextEditors.find((e) => { 83 | return e.document.uri === original_editor_uri; 84 | }); 85 | if (original_editor) { 86 | editor = original_editor; 87 | } 88 | } 89 | // don't return, remove all other things too -- we are here because Esc in the comment thread 90 | } 91 | if (editor) { 92 | let state = estate.state_of_editor(editor, "pressed_escape"); 93 | global.side_panel?.toolEditChange(editor.document.uri.fsPath, false); 94 | if (state) { 95 | state.diff_lens_pos = Number.MAX_SAFE_INTEGER; 96 | state.completion_lens_pos = Number.MAX_SAFE_INTEGER; 97 | codeLens.quick_refresh(); 98 | } 99 | if (state && (state.get_mode() === Mode.Diff || state.get_mode() === Mode.DiffWait)) { 100 | if (state.get_mode() === Mode.DiffWait) { 101 | await fetchAPI.cancel_all_requests_and_wait_until_finished(); 102 | } 103 | if (state.highlight_json_backup !== undefined) { 104 | await estate.switch_mode(state, Mode.Highlight); 105 | } else { 106 | await estate.switch_mode(state, Mode.Normal); 107 | } 108 | } else if (state && state.get_mode() === Mode.Highlight) { 109 | await estate.back_to_normal(state); 110 | } 111 | if (state && state.get_mode() === Mode.Normal) { 112 | await vscode.commands.executeCommand('setContext', 'refactcx.runEsc', false); 113 | await vscode.commands.executeCommand('editor.action.inlineSuggest.hide'); 114 | console.log(["ESC OFF"]); 115 | } 116 | } 117 | } 118 | 119 | 120 | async function pressed_tab() 121 | { 122 | let editor = vscode.window.activeTextEditor; 123 | if (global.comment_disposables) { 124 | // let original_editor_uri = rconsoleProvider.refact_console_close(); 125 | let original_editor_uri = RefactConsoleProvider.close_all_consoles(); 126 | if (original_editor_uri !== undefined) { 127 | let original_editor = vscode.window.visibleTextEditors.find((e) => { 128 | return e.document.uri === original_editor_uri; 129 | }); 130 | if (original_editor) { 131 | editor = original_editor; 132 | } 133 | } 134 | // fall through, accept the diff 135 | } 136 | if (editor) { 137 | let state = estate.state_of_editor(editor, "pressed_tab"); 138 | if (state && state.get_mode() === Mode.Diff) { 139 | interactiveDiff.like_and_accept(editor); 140 | } else { 141 | vscode.commands.executeCommand("setContext", "refactcx.runTab", false); 142 | } 143 | } 144 | } 145 | 146 | 147 | async function code_lens_clicked(arg0: any, arg1: any, range: vscode.Range) 148 | { 149 | let editor = vscode.window.activeTextEditor; 150 | if (editor) { 151 | let state = estate.state_of_editor(editor, "code_lens_clicked"); 152 | if (!state) { 153 | return; 154 | } 155 | if (arg0 === "APPROVE") { 156 | await interactiveDiff.like_and_accept(editor); 157 | // rconsoleProvider.refact_console_close(); 158 | RefactConsoleProvider.close_all_consoles(); 159 | } else if (arg0 === "REJECT") { 160 | await pressed_escape(); // might return to highlight 161 | } else if (arg0 === "RERUN") { 162 | await rollback_and_regen(editor); 163 | // } else if (arg0 === "COMP_APPROVE") { 164 | // state.completion_lens_pos = Number.MAX_SAFE_INTEGER; 165 | // codeLens.quick_refresh(); 166 | // await vscode.commands.executeCommand('editor.action.inlineSuggest.commit'); 167 | // } else if (arg0 === "COMP_REJECT") { 168 | // state.completion_lens_pos = Number.MAX_SAFE_INTEGER; 169 | // codeLens.quick_refresh(); 170 | // await vscode.commands.executeCommand('editor.action.inlineSuggest.hide'); 171 | // } else if (arg0 === "COMP_THINK_LONGER") { 172 | // state.completion_longthink = 1; 173 | // await vscode.commands.executeCommand('editor.action.inlineSuggest.hide'); 174 | // await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger'); 175 | } else { 176 | if (arg0.startsWith('CUSTOMLENS:')) { 177 | let custom_lens_name = arg0.substring("CUSTOMLENS:".length); 178 | code_lens_execute(custom_lens_name, range); 179 | } 180 | } 181 | } 182 | } 183 | 184 | async function f1_pressed() 185 | { 186 | pressed_call_chat(); 187 | } 188 | 189 | async function f1_deprecated() 190 | { 191 | let editor = vscode.window.activeTextEditor; 192 | if (editor) { 193 | let state = estate.state_of_editor(editor, "f1_pressed"); 194 | if (state && state.get_mode() === Mode.Diff) { 195 | rollback_and_regen(editor); 196 | return; 197 | } 198 | if (state) { 199 | RefactConsoleProvider.open_between_lines(editor); 200 | } 201 | } 202 | // await vscode.commands.executeCommand("refactai-toolbox.focus"); 203 | // await vscode.commands.executeCommand("workbench.action.focusSideBar"); 204 | } 205 | 206 | 207 | export async function inline_accepted(this_completion_serial_number: number) 208 | { 209 | if (typeof this_completion_serial_number === "number") { 210 | await completionProvider.inline_accepted(this_completion_serial_number); 211 | } else { 212 | console.log(["WARNING: inline_accepted no serial number!", this_completion_serial_number]); 213 | } 214 | } 215 | 216 | 217 | export function activate(context: vscode.ExtensionContext) 218 | { 219 | global.global_context = context; 220 | global.enable_longthink_completion = false; 221 | global.last_positive_result = 0; 222 | global.chat_models = []; 223 | global.have_caps = false; 224 | global.chat_default_model = ""; 225 | let disposable1 = vscode.commands.registerCommand('refactaicmd.inlineAccepted', inline_accepted); 226 | let disposable2 = vscode.commands.registerCommand('refactaicmd.codeLensClicked', code_lens_clicked); 227 | global.status_bar = new statusBar.StatusBarMenu(); 228 | global.status_bar.createStatusBarBlock(context); 229 | global.open_chat_tabs = []; 230 | global.toolbox_command_disposables = []; 231 | 232 | context.subscriptions.push(vscode.commands.registerCommand("refactaicmd.statusBarClick", status_bar_clicked)); 233 | 234 | codeLens.save_provider(new codeLens.LensProvider()); 235 | if (codeLens.global_provider) { 236 | context.subscriptions.push(vscode.languages.registerCodeLensProvider({ scheme: "file" }, codeLens.global_provider)); 237 | } 238 | 239 | const comp = new completionProvider.MyInlineCompletionProvider(); 240 | vscode.languages.registerInlineCompletionItemProvider({pattern: "**"}, comp); 241 | 242 | // const quickProvider = new QuickActionProvider(); 243 | // vscode.languages.registerCodeActionsProvider({pattern: "**"},quickProvider, 244 | // { 245 | // providedCodeActionKinds: [ 246 | // // vscode.CodeActionKind.RefactorRewrite, 247 | // vscode.CodeActionKind.QuickFix, 248 | // ], 249 | // } 250 | // ); 251 | // context.subscriptions.push(quickProvider); 252 | 253 | // for (const action of QuickActionProvider.actions_static_list) { 254 | // context.subscriptions.push( 255 | // vscode.commands.registerCommand( 256 | // `refactcmd.${action.id}`, 257 | // (actionId: string, diagnosticMessage: string) => QuickActionProvider.handleAction(actionId, diagnosticMessage) 258 | // ) 259 | // ); 260 | // } 261 | 262 | let disposable4 = vscode.commands.registerCommand('refactaicmd.esc', pressed_escape); 263 | let disposable5 = vscode.commands.registerCommand('refactaicmd.tab', pressed_tab); 264 | let disposable3 = vscode.commands.registerCommand('refactaicmd.activateToolbox', f1_pressed); 265 | let disposable8 = vscode.commands.registerCommand('refactaicmd.activateToolboxDeprecated', f1_deprecated); 266 | let disposable13 = vscode.commands.registerCommand('refactaicmd.completionManual', async () => { 267 | await vscode.commands.executeCommand('editor.action.inlineSuggest.hide'); 268 | await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger'); 269 | }); 270 | let disposable6 = vscode.commands.registerCommand('refactaicmd.callChat', pressed_call_chat); 271 | 272 | let toolbar_command_disposable = new vscode.Disposable(() => { 273 | global.toolbox_command_disposables.forEach(d => d.dispose()); 274 | }); 275 | 276 | context.subscriptions.push(disposable1); 277 | context.subscriptions.push(disposable2); 278 | context.subscriptions.push(disposable3); 279 | context.subscriptions.push(disposable4); 280 | context.subscriptions.push(disposable5); 281 | context.subscriptions.push(disposable8); 282 | context.subscriptions.push(disposable13); 283 | context.subscriptions.push(disposable6); 284 | context.subscriptions.push(toolbar_command_disposable); 285 | 286 | context.subscriptions.push(vscode.commands.registerCommand("refactaicmd.attachFile", (file) => { 287 | if(file.scheme !== "file") { return; } 288 | global.side_panel?.attachFile(file.fsPath); 289 | })); 290 | 291 | global.rust_binary_blob = new launchRust.RustBinaryBlob( 292 | fileURLToPath(vscode.Uri.joinPath(context.extensionUri, "assets").toString()) 293 | ); 294 | global.rust_binary_blob 295 | .settings_changed() // async function will finish later 296 | .then(() => fetchAPI.maybe_show_rag_status()); 297 | 298 | global.side_panel = new sidebar.PanelWebview(context); 299 | let view = vscode.window.registerWebviewViewProvider( 300 | 'refactai-toolbox', 301 | global.side_panel, 302 | {webviewOptions: {retainContextWhenHidden: true}} 303 | ); 304 | context.subscriptions.push(view); 305 | 306 | let settingsCommand = vscode.commands.registerCommand('refactaicmd.openSettings', () => { 307 | vscode.commands.executeCommand( 'workbench.action.openSettings', '@ext:smallcloud.codify' ); 308 | }); 309 | context.subscriptions.push(settingsCommand); 310 | 311 | let logout = vscode.commands.registerCommand('refactaicmd.logout', async () => { 312 | context.globalState.update('codifyFirstRun', false); 313 | await vscode.workspace.getConfiguration().update('refactai.apiKey', undefined, vscode.ConfigurationTarget.Global); 314 | await vscode.workspace.getConfiguration().update('refactai.addressURL', undefined, vscode.ConfigurationTarget.Global); 315 | await vscode.workspace.getConfiguration().update('codify.apiKey', undefined, vscode.ConfigurationTarget.Global); 316 | await vscode.workspace.getConfiguration().update('refactai.apiKey', undefined, vscode.ConfigurationTarget.Workspace); 317 | await vscode.workspace.getConfiguration().update('refactai.addressURL', undefined, vscode.ConfigurationTarget.Workspace); 318 | await vscode.workspace.getConfiguration().update('codify.apiKey', undefined, vscode.ConfigurationTarget.Workspace); 319 | global.status_bar.choose_color(); 320 | vscode.commands.executeCommand("workbench.action.webview.reloadWebviewAction"); 321 | }); 322 | 323 | context.subscriptions.push(logout); 324 | context.subscriptions.push(...statusBar.status_bar_init()); 325 | context.subscriptions.push(...estate.estate_init()); 326 | 327 | const home = path.posix.format(path.parse(os.homedir())); 328 | const toolbox_config_file_posix_path = path.posix.join( 329 | home, 330 | ".cache", 331 | "refact", 332 | "customization.yaml" 333 | ); 334 | 335 | const toolbox_config_file_uri = vscode.Uri.file(toolbox_config_file_posix_path); 336 | 337 | const openPromptCustomizationPage = vscode.commands.registerCommand( 338 | "refactaicmd.openPromptCustomizationPage", 339 | () => vscode.commands.executeCommand("vscode.open", toolbox_config_file_uri) 340 | ); 341 | 342 | context.subscriptions.push(openPromptCustomizationPage); 343 | 344 | const reloadOnCommandFileChange = vscode.workspace.onDidSaveTextDocument(document => { 345 | if(document.fileName === toolbox_config_file_uri.fsPath) { 346 | global.rust_binary_blob?.fetch_toolbox_config(); 347 | } 348 | }); 349 | 350 | context.subscriptions.push(reloadOnCommandFileChange); 351 | 352 | 353 | let config_debounce: NodeJS.Timeout|undefined; 354 | vscode.workspace.onDidChangeConfiguration(e => { 355 | // TODO: update commands here? 356 | if ( 357 | e.affectsConfiguration("refactai.infurl") || 358 | e.affectsConfiguration("refactai.addressURL") || 359 | e.affectsConfiguration("refactai.xDebug") || 360 | e.affectsConfiguration("refactai.apiKey") || 361 | e.affectsConfiguration("refactai.insecureSSL") || 362 | e.affectsConfiguration("refactai.ast") || 363 | e.affectsConfiguration("refactai.astFileLimit") || 364 | e.affectsConfiguration("refactai.vecdb") || 365 | e.affectsConfiguration("refactai.vecdbFileLimit") || 366 | e.affectsConfiguration("refactai.xperimental") 367 | ) { 368 | if (config_debounce) { 369 | clearTimeout(config_debounce); 370 | } 371 | config_debounce = setTimeout(() => { 372 | if (global.rust_binary_blob) { 373 | global.rust_binary_blob.settings_changed(); 374 | } 375 | }, 1000); 376 | } 377 | 378 | if (e.affectsConfiguration("refactai.apiKey") || e.affectsConfiguration("refactai.addressURL")) { 379 | global.side_panel?.handleSettingsChange(); 380 | } 381 | 382 | if ( 383 | e.affectsConfiguration("refactai.ast") || 384 | e.affectsConfiguration("refactai.astFileLimit") || 385 | e.affectsConfiguration("refactai.vecdb") || 386 | e.affectsConfiguration("refactai.vecdbFileLimit") 387 | ) { 388 | const hasAst = vscode.workspace.getConfiguration().get("refactai.ast"); 389 | if(hasAst) { 390 | fetchAPI.maybe_show_rag_status(); 391 | } 392 | const hasVecdb = vscode.workspace.getConfiguration().get("refactai.vecdb"); 393 | if(hasVecdb) { 394 | fetchAPI.maybe_show_rag_status(); 395 | } 396 | } 397 | }); 398 | 399 | const quickProvider = new QuickActionProvider(); 400 | context.subscriptions.push( 401 | vscode.languages.registerCodeActionsProvider( 402 | { pattern: "**" }, 403 | quickProvider, 404 | { 405 | providedCodeActionKinds: QuickActionProvider.providedCodeActionKinds 406 | } 407 | ) 408 | ); 409 | } 410 | 411 | export async function rollback_and_regen(editor: vscode.TextEditor) 412 | { 413 | let state = estate.state_of_editor(editor, "rollback_and_regen"); 414 | if (state) { 415 | await estate.switch_mode(state, Mode.Normal); // dislike_and_rollback inside 416 | // await interactiveDiff.query_the_same_thing_again(editor); 417 | } 418 | } 419 | 420 | 421 | export async function ask_and_save_intent(): Promise 422 | { 423 | let editor = vscode.window.activeTextEditor; 424 | if (!editor) { 425 | return false; 426 | } 427 | let selection = editor.selection; 428 | let selection_empty = selection.isEmpty; 429 | let intent: string | undefined = estate.global_intent; 430 | intent = await vscode.window.showInputBox({ 431 | title: (selection_empty ? 432 | "What would you like to do? (this action highlights code first)" : 433 | "What would you like to do with the selected code?"), 434 | value: estate.global_intent, 435 | valueSelection: [0, 80], 436 | placeHolder: 'Convert to list comprehension', 437 | }); 438 | if (intent) { 439 | estate.save_intent(intent); 440 | return true; 441 | } 442 | return false; 443 | } 444 | 445 | 446 | // export async function follow_intent_highlight(intent: string, function_name: string, model_name: string, third_party: boolean) 447 | // { 448 | // let editor = vscode.window.activeTextEditor; 449 | // if (!editor) { 450 | // return; 451 | // } 452 | // if (!intent) { 453 | // return; 454 | // } 455 | // await highlight.query_highlight(editor, intent, function_name, model_name, third_party); 456 | // } 457 | 458 | // export async function follow_intent_diff(intent: string, function_name: string, model_name: string, third_party: boolean) 459 | // { 460 | // let editor = vscode.window.activeTextEditor; 461 | // if (!editor) { 462 | // return; 463 | // } 464 | // if (!intent) { 465 | // return; 466 | // } 467 | // let selection = editor.selection; 468 | // // empty selection will become current line selection 469 | // editor.selection = new vscode.Selection(selection.start, selection.start); // this clears the selection, moves cursor up 470 | // if (selection.end.line > selection.start.line && selection.end.character === 0) { 471 | // let end_pos_in_chars = editor.document.lineAt(selection.end.line - 1).range.end.character; 472 | // selection = new vscode.Selection( 473 | // selection.start, 474 | // new vscode.Position(selection.end.line - 1, end_pos_in_chars) 475 | // ); 476 | // } 477 | // estate.save_intent(intent); 478 | // await interactiveDiff.query_diff(editor, selection, function_name || "diff-selection", model_name, third_party); 479 | // } 480 | 481 | 482 | export async function deactivate(context: vscode.ExtensionContext) 483 | { 484 | // global.global_context = undefined; 485 | if (global.rust_binary_blob) { 486 | await global.rust_binary_blob.terminate(); 487 | global.rust_binary_blob = undefined; 488 | } 489 | } 490 | 491 | 492 | export async function status_bar_clicked() 493 | { 494 | let editor = vscode.window.activeTextEditor; 495 | if (!userLogin.secret_api_key()) { 496 | userLogin.login_message(); 497 | return; 498 | } 499 | let selection: string | undefined; 500 | 501 | if (global.status_bar.ast_limit_hit || global.status_bar.vecdb_limit_hit) { 502 | selection = await vscode.window.showInformationMessage( 503 | "AST or VecDB file number limit reached, you can increase the limit in settings if your computer has enough memory, or disable these features.", 504 | "Open Settings", 505 | ); 506 | if (selection === "Open Settings") { 507 | await vscode.commands.executeCommand("workbench.action.openSettings", "@ext:smallcloud.codify"); 508 | } 509 | } else if (!editor) { 510 | selection = await vscode.window.showInformationMessage( 511 | "Welcome to Refact.ai 👋", 512 | "Open Panel (F1)", 513 | ); 514 | } else { 515 | // let document_filename = editor.document.fileName; 516 | // let chunks = document_filename.split("/"); 517 | let pause_completion = vscode.workspace.getConfiguration().get('refactai.pauseCompletion'); 518 | let buttons: string[] = []; 519 | buttons.push(pause_completion ? "Resume Completion" : "Pause Completion"); 520 | buttons.push("Open Panel (F1)"); 521 | selection = await vscode.window.showInformationMessage( 522 | "You can access Refact settings in the left side panel, look for |{ icon", 523 | ...buttons 524 | ); 525 | } 526 | if (selection === "Pause Completion") { 527 | vscode.workspace.getConfiguration().update('refactai.pauseCompletion', true, true); 528 | } else if (selection === "Resume Completion") { 529 | vscode.workspace.getConfiguration().update('refactai.pauseCompletion', false, true); 530 | } else if (selection === "Privacy Rules") { 531 | vscode.commands.executeCommand("refactaicmd.privacySettings"); 532 | } else if (selection === "Open Panel (F1)") { 533 | vscode.commands.executeCommand("refactaicmd.callChat"); 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /src/fetchAPI.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as fetchH2 from 'fetch-h2'; 4 | import * as usabilityHints from "./usabilityHints"; 5 | import * as estate from "./estate"; 6 | import * as statusBar from "./statusBar"; 7 | import { 8 | type CapsResponse, 9 | type CustomPromptsResponse, 10 | ChatMessages, 11 | } from "refact-chat-js/dist/events"; 12 | 13 | 14 | let globalSeq = 100; 15 | 16 | 17 | export class PendingRequest { 18 | seq: number; 19 | apiPromise: Promise | undefined; 20 | api_fields: estate.ApiFields | undefined; 21 | cancelToken: vscode.CancellationToken; 22 | cancellationTokenSource: vscode.CancellationTokenSource | undefined; 23 | streaming_callback: Function | undefined; 24 | streaming_end_callback: Function | undefined; 25 | streaming_buf: string = ""; 26 | streaming_error: string = ""; 27 | 28 | constructor(apiPromise: Promise | undefined, cancelToken: vscode.CancellationToken) 29 | { 30 | this.seq = globalSeq++; 31 | this.apiPromise = apiPromise; 32 | this.cancelToken = cancelToken; 33 | } 34 | 35 | set_streaming_callback(callback: Function | undefined, end_callback: Function | undefined) 36 | { 37 | this.streaming_callback = callback; 38 | this.streaming_end_callback = end_callback; 39 | } 40 | 41 | private async look_for_completed_data_in_streaming_buf() 42 | { 43 | let to_eat = ""; 44 | while (1) { 45 | let split_slash_n_slash_n = this.streaming_buf.split("\n\n"); 46 | if (split_slash_n_slash_n.length <= 1) { 47 | return; 48 | } 49 | let first = split_slash_n_slash_n[0]; 50 | this.streaming_buf = split_slash_n_slash_n.slice(1).join("\n\n"); 51 | if (first.substring(0, 6) !== "data: ") { 52 | console.log("Unexpected data in streaming buf: " + first); 53 | continue; 54 | } 55 | to_eat = first.substring(6); 56 | if (to_eat === "[DONE]") { 57 | if (this.streaming_end_callback) { 58 | // The normal way to end the streaming 59 | let my_cb = this.streaming_end_callback; 60 | this.streaming_end_callback = undefined; 61 | await my_cb(this.streaming_error); 62 | } 63 | break; 64 | } 65 | if (to_eat === "[ERROR]") { 66 | console.log("Streaming error"); 67 | this.streaming_error = "[ERROR]"; 68 | break; 69 | } 70 | let json = JSON.parse(to_eat); 71 | let error_detail = json["detail"]; 72 | if (typeof error_detail === "string") { 73 | this.streaming_error = error_detail; 74 | break; 75 | } 76 | if (this.streaming_callback) { 77 | await this.streaming_callback(json); 78 | } 79 | } 80 | } 81 | 82 | supply_stream(h2stream: Promise, scope: string, url: string) 83 | { 84 | this.streaming_error = ""; 85 | h2stream.catch((error) => { 86 | let aborted = error && error.message && error.message.includes("aborted"); 87 | if (!aborted) { 88 | console.log(["h2stream error (1)", error]); 89 | statusBar.send_network_problems_to_status_bar(false, scope, url, error, ""); 90 | } else { 91 | // Normal, user cancelled the request. 92 | } 93 | return; 94 | }); 95 | this.apiPromise = new Promise((resolve, reject) => { 96 | h2stream.then(async (result_stream) => { 97 | if (this.streaming_callback) { 98 | // Streaming is a bit homegrown, maybe read the docs: 99 | // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch 100 | // https://nodejs.org/api/stream.html#stream_readable_readablehighwatermark 101 | let readable = await result_stream.readable(); 102 | readable.on("readable", async () => { 103 | // Use readable here because we need to read as much as possible, feed the last 104 | // chunk only if model+network is faster than the GUI 105 | while (1) { 106 | let chunk = readable.read(); 107 | if (chunk === null) { 108 | break; 109 | } 110 | if (typeof chunk === "string") { 111 | this.streaming_buf += chunk; 112 | // console.log(["readable data", chunk]); 113 | } else { 114 | this.streaming_buf += chunk.toString(); 115 | // console.log(["readable data", chunk.toString()]); 116 | } 117 | await this.look_for_completed_data_in_streaming_buf(); 118 | } 119 | }); 120 | readable.on("close", async () => { 121 | // console.log(["readable end", this.streaming_buf]); 122 | if (this.streaming_buf.startsWith("{")) { 123 | // likely a error, because it's not a stream, no "data: " prefix 124 | console.log(["looks like a error", this.streaming_buf]); 125 | let error_message: string; 126 | try { 127 | let j = JSON.parse(this.streaming_buf); 128 | error_message = j["detail"]; 129 | if (typeof error_message !== "string") { 130 | error_message = this.streaming_buf; 131 | } 132 | } catch (e) { 133 | console.log(["error parsing error json", e]); 134 | error_message = this.streaming_buf; // as a string 135 | } 136 | this.streaming_error = error_message; 137 | // statusBar.send_network_problems_to_status_bar(false, scope, url, this.streaming_buf, ""); 138 | } else if (this.streaming_error) { 139 | // statusBar.send_network_problems_to_status_bar(false, scope, url, "streaming_error", ""); 140 | } else { 141 | // statusBar.send_network_problems_to_status_bar(true, scope, url, "", ""); 142 | } 143 | // Normally [DONE] produces a callback, but it's possible there's no [DONE] sent by the server. 144 | // Wait 500ms because inside VS Code "readable" and "end"/"close" are sometimes called in the wrong order. 145 | await new Promise(resolve => setTimeout(resolve, 500)); 146 | if (this.streaming_end_callback) { 147 | let my_cb = this.streaming_end_callback; 148 | this.streaming_end_callback = undefined; 149 | await my_cb(this.streaming_error); 150 | } 151 | }); 152 | resolve(""); 153 | } else { 154 | // not streaming 155 | let json_arrived = await result_stream.json(); 156 | if (json_arrived.inference_message) { 157 | // It's async, potentially two messages might appear if requests are fast, but we don't launch new requests 158 | // until the previous one is finished, should be fine... 159 | usabilityHints.show_message_from_server("InferenceServer", json_arrived.inference_message); 160 | } 161 | if (look_for_common_errors(json_arrived, scope, "")) { 162 | reject(); 163 | return; 164 | } 165 | let model_name = json_arrived["model"]; 166 | if (typeof json_arrived === "object" && json_arrived.length !== undefined) { 167 | model_name = json_arrived[0]["model"]; 168 | } 169 | statusBar.send_network_problems_to_status_bar(true, scope, url, "", model_name); 170 | resolve(json_arrived); 171 | } 172 | }).catch(async (error) => { 173 | let aborted = error && error.message && error.message.includes("aborted"); 174 | if (!aborted) { 175 | console.log(["h2stream error (2)", error]); 176 | statusBar.send_network_problems_to_status_bar(false, scope, url, error, ""); 177 | } 178 | if (this.streaming_end_callback) { 179 | let my_cb = this.streaming_end_callback; 180 | this.streaming_end_callback = undefined; 181 | await my_cb(error !== undefined); 182 | } 183 | reject(); 184 | }); 185 | }).finally(() => { 186 | let index = _global_reqs.indexOf(this); 187 | if (index >= 0) { 188 | _global_reqs.splice(index, 1); 189 | } 190 | if (_global_reqs.length === 0) { 191 | global.status_bar.statusbar_spinner(false); 192 | } 193 | // console.log(["--pendingRequests", _global_reqs.length, request.seq]); 194 | }).catch((error) => { 195 | let aborted = error && error.message && error.message.includes("aborted"); 196 | if (error === undefined) { 197 | // This is a result of reject() without parameters 198 | return; 199 | } else if (!aborted) { 200 | console.log(["h2stream error (3)", error]); 201 | statusBar.send_network_problems_to_status_bar(false, scope, url, error, ""); 202 | } 203 | }); 204 | _global_reqs.push(this); 205 | global.status_bar.statusbar_spinner(true); 206 | // console.log(["++pendingRequests", _global_reqs.length, request.seq]); 207 | } 208 | } 209 | 210 | 211 | let _global_reqs: PendingRequest[] = []; 212 | 213 | 214 | export async function wait_until_all_requests_finished() 215 | { 216 | for (let i=0; i<_global_reqs.length; i++) { 217 | let r = _global_reqs[i]; 218 | if (r.apiPromise !== undefined) { 219 | console.log([r.seq, "wwwwwwwwwwwwwwwww"]); 220 | let tmp = await r.apiPromise; 221 | r.apiPromise = undefined; 222 | } 223 | } 224 | } 225 | 226 | export function anything_still_working() 227 | { 228 | for (let i=0; i<_global_reqs.length; i++) { 229 | let r = _global_reqs[i]; 230 | if (!r.cancelToken.isCancellationRequested) { 231 | return true; 232 | } 233 | } 234 | return false; 235 | } 236 | 237 | export async function cancel_all_requests_and_wait_until_finished() 238 | { 239 | for (let i=0; i<_global_reqs.length; i++) { 240 | let r = _global_reqs[i]; 241 | if (r.cancellationTokenSource !== undefined) { 242 | r.cancellationTokenSource.cancel(); 243 | } 244 | } 245 | await wait_until_all_requests_finished(); 246 | } 247 | 248 | 249 | export let global_inference_url_from_login = ""; 250 | 251 | 252 | export function save_url_from_login(url: string) 253 | { 254 | global_inference_url_from_login = url; 255 | } 256 | 257 | 258 | export function rust_url(addthis: string) 259 | { 260 | if (!global.rust_binary_blob) { 261 | return ""; 262 | } 263 | let url = global.rust_binary_blob.rust_url(); 264 | while (url.endsWith("/")) { 265 | url = url.slice(0, -1); 266 | } 267 | url += addthis; 268 | return url; 269 | } 270 | 271 | 272 | export function inference_context(third_party: boolean) 273 | { 274 | // let modified_url = vscode.workspace.getConfiguration().get('refactai.infurl'); 275 | // if (!modified_url) { 276 | // // Backward compatibility: codify is the old name 277 | // modified_url = vscode.workspace.getConfiguration().get('codify.infurl'); 278 | // } 279 | // in previous versions, it was possible to skip certificate verification 280 | return { 281 | disconnect: fetchH2.disconnect, 282 | disconnectAll: fetchH2.disconnectAll, 283 | fetch: fetchH2.fetch, 284 | onPush: fetchH2.onPush, 285 | setup: fetchH2.setup, 286 | }; 287 | } 288 | 289 | 290 | export function fetch_code_completion( 291 | cancelToken: vscode.CancellationToken, 292 | sources: { [key: string]: string }, 293 | multiline: boolean, 294 | cursor_file: string, 295 | cursor_line: number, 296 | cursor_character: number, 297 | max_new_tokens: number, 298 | no_cache: boolean, 299 | temperature: number, 300 | // api_fields: estate.ApiFields, 301 | ): Promise 302 | { 303 | let url = rust_url("/v1/code-completion"); 304 | if (!url) { 305 | console.log(["fetch_code_completion: No rust binary working"]); 306 | return Promise.reject("No rust binary working"); 307 | } 308 | let third_party = false; 309 | let ctx = inference_context(third_party); 310 | let model_name = vscode.workspace.getConfiguration().get("refactai.codeCompletionModel") || ""; 311 | let client_version = vscode.extensions.getExtension("smallcloud.codify")!.packageJSON.version; 312 | // api_fields.scope = "code-completion"; 313 | // api_fields.url = url; 314 | // api_fields.model = model; 315 | // api_fields.sources = sources; 316 | // api_fields.intent = ""; 317 | // api_fields.function = "completion"; 318 | // api_fields.cursor_file = cursor_file; 319 | // api_fields.cursor_pos0 = -1; 320 | // api_fields.cursor_pos1 = -1; 321 | // api_fields.ts_req = Date.now(); 322 | let use_ast = vscode.workspace.getConfiguration().get("refactai.ast"); 323 | 324 | const post = JSON.stringify({ 325 | "model": model_name, 326 | "inputs": { 327 | "sources": sources, 328 | "cursor": { 329 | "file": cursor_file, 330 | "line": cursor_line, 331 | "character": cursor_character, 332 | }, 333 | "multiline": multiline, 334 | }, 335 | "parameters": { 336 | "temperature": temperature, 337 | "max_new_tokens": max_new_tokens, 338 | }, 339 | "no_cache": no_cache, 340 | "use_ast": use_ast, 341 | "client": `vscode-${client_version}`, 342 | }); 343 | const headers = { 344 | "Content-Type": "application/json", 345 | // "Authorization": `Bearer ${apiKey}`, 346 | }; 347 | let req = new fetchH2.Request(url, { 348 | method: "POST", 349 | headers: headers, 350 | body: post, 351 | redirect: "follow", 352 | cache: "no-cache", 353 | referrer: "no-referrer" 354 | }); 355 | let init: any = { 356 | timeout: 20*1000, 357 | }; 358 | if (cancelToken) { 359 | let abort = new fetchH2.AbortController(); 360 | cancelToken.onCancellationRequested(async () => { 361 | console.log(["API fetch cancelled"]); 362 | abort.abort(); 363 | 364 | global.side_panel?.chat?.handleStreamEnd(); 365 | 366 | await fetchH2.disconnectAll(); 367 | }); 368 | init.signal = abort.signal; 369 | } 370 | let promise = ctx.fetch(req, init); 371 | return promise; 372 | } 373 | 374 | 375 | export function fetch_chat_promise( 376 | cancelToken: vscode.CancellationToken, 377 | scope: string, 378 | messages: ChatMessages | [string, string][], 379 | model: string, 380 | third_party: boolean = false, 381 | tools: AtToolCommand[] | null = null, 382 | ): [Promise, string, string] 383 | { 384 | let url = rust_url("/v1/chat"); 385 | if (!url) { 386 | console.log(["fetch_chat_promise: No rust binary working"]); 387 | return [Promise.reject("No rust binary working"), scope, ""]; 388 | } 389 | const apiKey = "any-key-will-work"; 390 | if (!apiKey) { 391 | return [Promise.reject("No API key"), "chat", ""]; 392 | } 393 | 394 | let ctx = inference_context(third_party); 395 | 396 | // an empty tools array causes issues 397 | const maybeTools = tools && tools.length > 0 ? {tools} : {}; 398 | const body = JSON.stringify({ 399 | "messages": [], //json_messages, 400 | "model": model, 401 | "parameters": { 402 | "max_new_tokens": 1000, 403 | }, 404 | "stream": true, 405 | ...maybeTools 406 | }); 407 | 408 | const headers = { 409 | "Content-Type": "application/json", 410 | "Authorization": `Bearer ${apiKey}`, 411 | }; 412 | 413 | let req = new fetchH2.Request(url, { 414 | method: "POST", 415 | headers: headers, 416 | body: body, 417 | redirect: "follow", 418 | cache: "no-cache", 419 | referrer: "no-referrer" 420 | }); 421 | let init: any = { 422 | timeout: 20*1000, 423 | }; 424 | if (cancelToken) { 425 | let abort = new fetchH2.AbortController(); 426 | cancelToken.onCancellationRequested(() => { 427 | console.log(["chat cancelled"]); 428 | abort.abort(); 429 | }); 430 | init.signal = abort.signal; 431 | } 432 | let promise = ctx.fetch(req, init); 433 | return [promise, scope, ""]; 434 | } 435 | 436 | 437 | export function look_for_common_errors(json: any, scope: string, url: string): boolean 438 | { 439 | if (json === undefined) { 440 | // undefined means error is already handled, do nothing 441 | return true; 442 | } 443 | if (json.detail) { 444 | statusBar.send_network_problems_to_status_bar(false, scope, url, json.detail, ""); 445 | return true; 446 | } 447 | if (json.retcode && json.retcode !== "OK") { 448 | statusBar.send_network_problems_to_status_bar(false, scope, url, json.human_readable_message, ""); 449 | return true; 450 | } 451 | if (json.error) { 452 | if (typeof json.error === "string") { 453 | statusBar.send_network_problems_to_status_bar(false, scope, url, json.error, ""); 454 | } else { 455 | statusBar.send_network_problems_to_status_bar(false, scope, url, json.error.message, ""); 456 | } 457 | } 458 | return false; 459 | } 460 | 461 | export async function get_caps(): Promise { 462 | let url = rust_url("/v1/caps"); 463 | if (!url) { 464 | return Promise.reject("read_caps no rust binary working, very strange"); 465 | } 466 | 467 | let req = new fetchH2.Request(url, { 468 | method: "GET", 469 | redirect: "follow", 470 | cache: "no-cache", 471 | referrer: "no-referrer", 472 | }); 473 | 474 | let resp = await fetchH2.fetch(req); 475 | if (resp.status !== 200) { 476 | console.log(["read_caps http status", resp.status]); 477 | return Promise.reject("read_caps bad status"); 478 | } 479 | let json = await resp.json(); 480 | console.log(["successful read_caps", json]); 481 | return json as CapsResponse; 482 | } 483 | 484 | export async function get_prompt_customization(): Promise { 485 | const url = rust_url("/v1/customization"); 486 | 487 | if (!url) { 488 | return Promise.reject("unable to get prompt customization"); 489 | } 490 | 491 | const request = new fetchH2.Request(url, { 492 | method: "GET", 493 | redirect: "follow", 494 | cache: "no-cache", 495 | referrer: "no-referrer", 496 | }); 497 | 498 | const response = await fetchH2.fetch(request); 499 | 500 | if (!response.ok) { 501 | console.log(["get_prompt_customization http status", response.status]); 502 | return Promise.reject("unable to get prompt customization"); 503 | } 504 | 505 | const json = await response.json(); 506 | 507 | return json; 508 | } 509 | 510 | export type AstStatus = { 511 | files_unparsed: number; 512 | files_total: number; 513 | ast_index_files_total: number; 514 | ast_index_symbols_total: number; 515 | state: "starting" | "parsing" | "indexing" | "done"; 516 | }; 517 | 518 | export interface RagStatus { 519 | ast: { 520 | files_unparsed: number; 521 | files_total: number; 522 | ast_index_files_total: number; 523 | ast_index_symbols_total: number; 524 | state: string; 525 | ast_max_files_hit: boolean; 526 | } | null; 527 | ast_alive: string | null; 528 | vecdb: { 529 | files_unprocessed: number; 530 | files_total: number; 531 | requests_made_since_start: number; 532 | vectors_made_since_start: number; 533 | db_size: number; 534 | db_cache_size: number; 535 | state: string; 536 | vecdb_max_files_hit: boolean; 537 | } | null; 538 | vecdb_alive: string | null; 539 | vec_db_error: string; 540 | } 541 | 542 | async function fetch_rag_status() 543 | { 544 | const url = rust_url("/v1/rag-status"); 545 | if(!url) { 546 | return Promise.reject("rag-status no rust binary working, very strange"); 547 | } 548 | 549 | const request = new fetchH2.Request(url, { 550 | method: "GET", 551 | redirect: "follow", 552 | cache: "no-cache", 553 | referrer: "no-referrer", 554 | }); 555 | 556 | try { 557 | const response = await fetchH2.fetch(request); 558 | if (response.status !== 200) { 559 | console.log(["rag-status http status", response.status]); 560 | } 561 | const json = await response.json(); 562 | return json; 563 | } catch (e) { 564 | statusBar.send_network_problems_to_status_bar( 565 | false, 566 | "rag-status", 567 | url, 568 | e, 569 | undefined 570 | ); 571 | } 572 | return Promise.reject("rag-status bad status"); 573 | } 574 | 575 | let ragstat_timeout: NodeJS.Timeout | undefined; 576 | 577 | export function maybe_show_rag_status(statusbar: statusBar.StatusBarMenu = global.status_bar) 578 | { 579 | if (ragstat_timeout) { 580 | clearTimeout(ragstat_timeout); 581 | ragstat_timeout = undefined; 582 | } 583 | 584 | fetch_rag_status() 585 | .then((res: RagStatus) => { 586 | if (res.ast && res.ast.ast_max_files_hit) { 587 | statusbar.ast_status_limit_reached(); 588 | ragstat_timeout = setTimeout(() => maybe_show_rag_status(statusbar), 5000); 589 | return; 590 | } 591 | 592 | if (res.vecdb && res.vecdb.vecdb_max_files_hit) { 593 | statusbar.vecdb_status_limit_reached(); 594 | ragstat_timeout = setTimeout(() => maybe_show_rag_status(statusbar), 5000); 595 | return; 596 | } 597 | 598 | statusbar.ast_limit_hit = false; 599 | statusbar.vecdb_limit_hit = false; 600 | 601 | if (res.vec_db_error !== '') { 602 | statusbar.vecdb_error(res.vec_db_error); 603 | } 604 | 605 | if ((res.ast && ["starting", "parsing", "indexing"].includes(res.ast.state)) || 606 | (res.vecdb && ["starting", "parsing", "cooldown"].includes(res.vecdb.state))) 607 | { 608 | // console.log("ast or vecdb is still indexing"); 609 | ragstat_timeout = setTimeout(() => maybe_show_rag_status(statusbar), 700); 610 | } else { 611 | // console.log("ast and vecdb status complete, slowdown poll"); 612 | statusbar.statusbar_spinner(false); 613 | ragstat_timeout = setTimeout(() => maybe_show_rag_status(statusbar), 5000); 614 | } 615 | statusbar.update_rag_status(res); 616 | }) 617 | .catch((err) => { 618 | console.log("fetch_rag_status", err); 619 | ragstat_timeout = setTimeout(() => maybe_show_rag_status(statusbar), 5000); 620 | }); 621 | } 622 | 623 | type AtParamDict = { 624 | name: string; 625 | type: string; 626 | description: string; 627 | }; 628 | 629 | type AtToolFunction = { 630 | name: string; 631 | agentic: boolean; 632 | description: string; 633 | parameters: AtParamDict[]; 634 | parameters_required: string[]; 635 | }; 636 | 637 | type AtToolCommand = { 638 | function: AtToolFunction; 639 | type: "function"; 640 | }; 641 | 642 | type AtToolResponse = AtToolCommand[]; 643 | 644 | export async function get_tools(notes: boolean = false): Promise { 645 | const url = rust_url("/v1/tools"); 646 | 647 | if (!url) { 648 | return Promise.reject("unable to get tools url"); 649 | } 650 | const request = new fetchH2.Request(url, { 651 | method: "GET", 652 | redirect: "follow", 653 | cache: "no-cache", 654 | referrer: "no-referrer", 655 | }); 656 | 657 | 658 | const response = await fetchH2.fetch(request); 659 | 660 | if (!response.ok) { 661 | console.log(["tools response http status", response.status]); 662 | 663 | // return Promise.reject("unable to get available tools"); 664 | return []; 665 | } 666 | 667 | const json: AtToolResponse = await response.json(); 668 | 669 | const tools = notes ? 670 | json.filter((tool) => tool.function.name === "note_to_self") : 671 | json.filter((tool) => tool.function.name !== "note_to_self"); 672 | 673 | return tools; 674 | } 675 | 676 | 677 | export async function lsp_set_active_document(editor: vscode.TextEditor) 678 | { 679 | let url = rust_url("/v1/lsp-set-active-document"); 680 | if (url) { 681 | const post = JSON.stringify({ 682 | "uri": editor.document.uri.toString(), 683 | }); 684 | const headers = { 685 | "Content-Type": "application/json", 686 | }; 687 | let req = new fetchH2.Request(url, { 688 | method: "POST", 689 | headers: headers, 690 | body: post, 691 | redirect: "follow", 692 | cache: "no-cache", 693 | referrer: "no-referrer" 694 | }); 695 | fetchH2.fetch(req).then((response) => { 696 | if (!response.ok) { 697 | console.log(["lsp-set-active-document failed", response.status, response.statusText]); 698 | } else { 699 | console.log(["lsp-set-active-document success", response.status]); 700 | } 701 | }); 702 | } 703 | } 704 | -------------------------------------------------------------------------------- /src/getKeybindings.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import path from 'path'; 3 | import JSON5 from 'json5'; 4 | 5 | const isInsiders = vscode.version.includes("insider"); 6 | 7 | const codeFolder = isInsiders ? "Code - Insiders" : "Code"; 8 | const configPaths = { 9 | windows: path.join(process.env.APPDATA || "", codeFolder), 10 | macos: path.join(process.env.HOME || "", "Library", "Application Support", codeFolder), 11 | linux: path.join(process.env.HOME || "", "config", codeFolder) 12 | }; 13 | 14 | 15 | function getSystem(): keyof typeof configPaths { 16 | switch (process.platform) { 17 | case "aix": return "linux"; 18 | case "darwin": return "macos"; 19 | case "freebsd": return "linux"; 20 | case "linux": return "linux"; 21 | case "openbsd": return "linux"; 22 | case "sunos": return "linux"; 23 | case "win32": return "windows"; 24 | default: return "windows"; 25 | } 26 | } 27 | 28 | function getPathToConfigFile(): string { 29 | // XXX: use any other way to read keybindings.json, so it doesn't get to the LSP 30 | const system = getSystem(); 31 | const directoryForSystem = configPaths[system]; 32 | const configDir = process.env.VSCODE_PORTABLE ? path.join(process.env.VSCODE_PORTABLE, "user-data","User") : directoryForSystem; 33 | 34 | const pathToFile = path.join(configDir, "User", "keybindings.json"); 35 | return pathToFile; 36 | 37 | } 38 | 39 | type Keybinding = { 40 | command: string; 41 | key: string; 42 | }; 43 | 44 | async function getUserConfig(path: string): Promise { 45 | try { 46 | const doc = await vscode.workspace.openTextDocument(path); 47 | const text = doc.getText(); 48 | const json: Keybinding[] = JSON5.parse(text); 49 | return json; 50 | } catch (e) { 51 | return []; 52 | } 53 | } 54 | 55 | export async function getKeybindings(key: string): Promise; 56 | 57 | export async function getKeybindings(): Promise>; 58 | 59 | export async function getKeybindings(key?: string): Promise> { 60 | const pathToConfigFile = getPathToConfigFile(); 61 | const defaultKeyBindings: Keybinding[] = require("../package.json").contributes.keybindings; 62 | const userConfig = await getUserConfig(pathToConfigFile); 63 | 64 | 65 | const allKeyBindings = [...defaultKeyBindings, ...userConfig]; 66 | const data = allKeyBindings.reduce>((a, b) => { 67 | a[b.command] = b.key; 68 | return a; 69 | }, {}); 70 | 71 | if(key){ 72 | return data[key]; 73 | } else { 74 | return data; 75 | } 76 | } 77 | 78 | export async function getKeyBindingForChat(name: string): Promise { 79 | const system = getSystem(); 80 | let key = await getKeybindings(name); 81 | 82 | if(system === "macos") { 83 | key = key 84 | .replace("alt", "⌥") 85 | .replace("ctrl", "⌃") 86 | .replace("cmd", "⌘") 87 | .toLocaleUpperCase() 88 | return key; 89 | } 90 | return key.replace(/\w+/g, w => (w.substring(0,1).toUpperCase()) + w.substring(1)); 91 | } -------------------------------------------------------------------------------- /src/launchRust.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as fetchH2 from 'fetch-h2'; 4 | import * as userLogin from './userLogin'; 5 | import * as fetchAPI from "./fetchAPI"; 6 | import { join } from 'path'; 7 | import * as lspClient from 'vscode-languageclient/node'; 8 | import * as net from 'net'; 9 | import { register_commands } from './rconsoleCommands'; 10 | import { QuickActionProvider } from './quickProvider'; 11 | import { TeamsGroup } from 'refact-chat-js/dist/events'; 12 | 13 | 14 | const DEBUG_HTTP_PORT = 8001; 15 | const DEBUG_LSP_PORT = 8002; 16 | 17 | 18 | export class RustBinaryBlob { 19 | public asset_path: string; 20 | public cmdline: string[] = []; 21 | public port: number = 0; 22 | public lsp_disposable: vscode.Disposable | undefined = undefined; 23 | public lsp_client: lspClient.LanguageClient | undefined = undefined; 24 | public lsp_socket: net.Socket | undefined = undefined; 25 | public lsp_client_options: lspClient.LanguageClientOptions; 26 | public ping_response: string = ""; 27 | 28 | constructor(asset_path: string) { 29 | this.asset_path = asset_path; 30 | this.lsp_client_options = { 31 | documentSelector: [{ scheme: 'file', language: '*' }], 32 | diagnosticCollectionName: 'RUST LSP', 33 | progressOnInitialization: true, 34 | traceOutputChannel: vscode.window.createOutputChannel('RUST LSP'), 35 | revealOutputChannelOn: lspClient.RevealOutputChannelOn.Error, 36 | }; 37 | } 38 | 39 | public x_debug(): number { 40 | let xdebug = vscode.workspace.getConfiguration().get("refactai.xDebug"); 41 | if (xdebug === undefined || xdebug === null || xdebug === 0 || xdebug === "0" || xdebug === false || xdebug === "false") { 42 | return 0; 43 | } 44 | return 1; 45 | } 46 | 47 | public get_port(): number { 48 | let xdebug = this.x_debug(); 49 | if (xdebug) { 50 | return 8001; 51 | } else { 52 | return this.port; 53 | } 54 | } 55 | 56 | public rust_url(): string { 57 | let xdebug = this.x_debug(); 58 | let port = xdebug ? 8001 : this.port; 59 | if (!port) { 60 | return ""; 61 | } 62 | return "http2://127.0.0.1:" + port.toString() + "/"; 63 | } 64 | 65 | public attemping_to_reach(): string { 66 | let xdebug = this.x_debug(); 67 | if (xdebug) { 68 | return `debug rust binary on ports ${DEBUG_HTTP_PORT} and ${DEBUG_LSP_PORT}`; 69 | } else { 70 | let addr = userLogin.get_address(); 71 | if (addr === "") { 72 | return ""; 73 | } 74 | return `${addr}`; 75 | } 76 | } 77 | 78 | public async settings_changed() { 79 | for (let i = 0; i < 5; i++) { 80 | console.log(`RUST settings changed, attempt to restart ${i + 1}`); 81 | let xdebug = this.x_debug(); 82 | let api_key: string = userLogin.secret_api_key(); 83 | let port: number; 84 | let ping_response: string; 85 | 86 | // const maybe_active_workspace = global.global_context.globalState.get('active_workspace') as Workspace | undefined; 87 | // const active_workspace_id = maybe_active_workspace ? maybe_active_workspace.workspace_id : null; 88 | if (xdebug === 0) { 89 | if (this.lsp_client) { // running 90 | port = this.port; // keep the same port 91 | ping_response = this.ping_response; 92 | } else { 93 | port = Math.floor(Math.random() * 20) + 9080; 94 | ping_response = `ping-${Math.floor(Math.random() * 0x10000000000000000).toString(16)}`; 95 | } 96 | } else { 97 | port = DEBUG_HTTP_PORT; 98 | console.log(`RUST debug is set, don't start the rust binary. Will attempt HTTP port ${DEBUG_HTTP_PORT}, LSP port ${DEBUG_LSP_PORT}`); 99 | console.log("Also, will try to read caps. If that fails, things like lists of available models will be empty."); 100 | this.cmdline = []; 101 | await this.terminate(); // terminate our own 102 | await this.read_caps(); // debugging rust already running, can read here 103 | 104 | await this.fetch_toolbox_config(); 105 | // await register_commands(); 106 | await this.start_lsp_socket(); 107 | return; 108 | } 109 | let url: string = userLogin.get_address(); 110 | if (url === "") { 111 | this.cmdline = []; 112 | await this.terminate(); 113 | return; 114 | } 115 | let plugin_version = vscode.extensions.getExtension("smallcloud.codify")?.packageJSON.version; // codify is the old name of the product, smallcloud is the company 116 | if (!plugin_version) { 117 | plugin_version = "unknown"; 118 | } 119 | 120 | let new_cmdline: string[] = [ 121 | join(this.asset_path, "refact-lsp"), 122 | "--address-url", url, 123 | "--api-key", api_key, 124 | "--ping-message", ping_response, 125 | "--http-port", port.toString(), 126 | "--lsp-stdin-stdout", "1", 127 | "--enduser-client-version", "refact-" + plugin_version + "/vscode-" + vscode.version, 128 | "--basic-telemetry", 129 | ]; 130 | 131 | // if (active_workspace_id !== null && active_workspace_id !== undefined) { 132 | // new_cmdline.push("--active-workspace-id"); 133 | // new_cmdline.push(active_workspace_id.toString()); 134 | // } 135 | 136 | if (vscode.workspace.getConfiguration().get("refactai.vecdb")) { 137 | new_cmdline.push("--vecdb"); 138 | const vecdb_limit = vscode.workspace.getConfiguration().get("refactai.vecdbFileLimit") ?? 15000; 139 | new_cmdline.push(`--vecdb-max-files`); 140 | new_cmdline.push(`${vecdb_limit}`); 141 | } 142 | if (vscode.workspace.getConfiguration().get("refactai.ast")) { 143 | new_cmdline.push("--ast"); 144 | const ast_limit = vscode.workspace.getConfiguration().get("refactai.astFileLimit") ?? 15000; 145 | new_cmdline.push(`--ast-max-files`); 146 | new_cmdline.push(`${ast_limit}`); 147 | } 148 | let insecureSSL = vscode.workspace.getConfiguration().get("refactai.insecureSSL"); 149 | if (insecureSSL) { 150 | new_cmdline.push("--insecure"); 151 | } 152 | let experimental = vscode.workspace.getConfiguration().get("refactai.xperimental"); 153 | if (experimental) { 154 | new_cmdline.push("--experimental"); 155 | } 156 | 157 | let cmdline_existing: string = this.cmdline.join(" "); 158 | let cmdline_new: string = new_cmdline.join(" "); 159 | if (cmdline_existing !== cmdline_new) { 160 | this.cmdline = new_cmdline; 161 | this.port = port; 162 | this.ping_response = ping_response; 163 | await this.launch(); 164 | } 165 | if (this.lsp_disposable !== undefined) { 166 | break; 167 | } 168 | } 169 | global.side_panel?.handleSettingsChange(); 170 | } 171 | 172 | public async launch() { 173 | await this.terminate(); 174 | let xdebug = this.x_debug(); 175 | if (xdebug) { 176 | await this.start_lsp_socket(); 177 | } else { 178 | await this.start_lsp_stdin_stdout(); 179 | } 180 | } 181 | 182 | public stop_lsp() { 183 | let my_lsp_client_copy = this.lsp_client; 184 | if (my_lsp_client_copy) { 185 | console.log("RUST STOP"); 186 | let ts = Date.now(); 187 | my_lsp_client_copy.stop() // will complete in the background, otherwise it might die inside stop() such that execution never reaches even finally() and we don't know how to restart the process :/ 188 | .then(() => { 189 | console.log(`RUST /STOP completed in ${Date.now() - ts}ms`); 190 | }) 191 | .catch((e) => { 192 | console.log(`RUST STOP ERROR e=${e}`); 193 | }) 194 | .finally(() => { 195 | console.log("RUST STOP FINALLY"); 196 | }); 197 | } 198 | this.lsp_dispose(); 199 | } 200 | 201 | public lsp_dispose() { 202 | if (this.lsp_disposable) { 203 | this.lsp_disposable.dispose(); 204 | this.lsp_disposable = undefined; 205 | } 206 | this.lsp_client = undefined; 207 | this.lsp_socket = undefined; 208 | } 209 | 210 | public async terminate() { 211 | this.stop_lsp(); 212 | await fetchH2.disconnectAll(); 213 | global.have_caps = false; 214 | global.status_bar.choose_color(); 215 | } 216 | 217 | public async read_caps() { 218 | try { 219 | let url = this.rust_url(); 220 | if (!url) { 221 | return Promise.reject("read_caps no rust binary working, very strange"); 222 | } 223 | url += "v1/caps"; 224 | let req = new fetchH2.Request(url, { 225 | method: "GET", 226 | redirect: "follow", 227 | cache: "no-cache", 228 | referrer: "no-referrer" 229 | }); 230 | let resp = await fetchH2.fetch(req); 231 | if (resp.status !== 200) { 232 | console.log(["read_caps http status", resp.status]); 233 | return Promise.reject("read_caps bad status"); 234 | } 235 | let json = await resp.json(); 236 | console.log(["successful read_caps", json]); 237 | global.chat_models = Object.keys(json["chat_models"]); 238 | global.chat_default_model = json["chat_default_model"] || ""; 239 | global.have_caps = true; 240 | global.status_bar.set_socket_error(false, ""); 241 | } catch (e) { 242 | global.chat_models = []; 243 | global.have_caps = false; 244 | console.log(["read_caps:", e]); 245 | } 246 | global.status_bar.choose_color(); 247 | fetchAPI.maybe_show_rag_status(); 248 | let current_editor = vscode.window.activeTextEditor; 249 | if (current_editor) { 250 | fetchAPI.lsp_set_active_document(current_editor); 251 | } 252 | 253 | const promptCustomization = await fetchAPI.get_prompt_customization(); 254 | if (promptCustomization && promptCustomization.toolbox_commands) { 255 | await QuickActionProvider.updateActions(promptCustomization.toolbox_commands as Record); 256 | } 257 | } 258 | 259 | public async ping() { 260 | try { 261 | let url = this.rust_url(); 262 | if (!url) { 263 | return Promise.reject("ping no rust binary working, very strange"); 264 | } 265 | url += "v1/ping"; 266 | console.log([url]); 267 | let req = new fetchH2.Request(url, { 268 | method: "GET", 269 | redirect: "follow", 270 | cache: "no-cache", 271 | referrer: "no-referrer", 272 | }); 273 | let resp = await fetchH2.fetch(req, { timeout: 5000 }); 274 | if (resp.status !== 200) { 275 | console.log(["ping http status", resp.status]); 276 | return Promise.reject("ping bad status"); 277 | } 278 | let pong = await resp.text(); 279 | let success = (pong === this.ping_response || pong === this.ping_response + "\n"); 280 | console.log([`pong=${pong}`, `expected ${this.ping_response}`, success]); 281 | return success; 282 | } catch (e) { 283 | console.log(["ping error:", e]); 284 | } 285 | return false; 286 | } 287 | 288 | public async start_lsp_stdin_stdout() { 289 | console.log("RUST start_lsp_stdint_stdout"); 290 | let path = this.cmdline[0]; 291 | let serverOptions: lspClient.ServerOptions; 292 | serverOptions = { 293 | run: { 294 | command: String(path), 295 | args: this.cmdline.slice(1), 296 | transport: lspClient.TransportKind.stdio, 297 | options: { cwd: process.cwd(), detached: false, shell: false } 298 | }, 299 | debug: { 300 | command: String(path), 301 | args: this.cmdline.slice(1), 302 | transport: lspClient.TransportKind.stdio, 303 | options: { cwd: process.cwd(), detached: false, shell: false } 304 | } 305 | }; 306 | this.lsp_client = new lspClient.LanguageClient( 307 | 'RUST LSP', 308 | serverOptions, 309 | this.lsp_client_options 310 | ); 311 | this.lsp_disposable = this.lsp_client.start(); 312 | 313 | console.log(`${logts()} RUST START`); 314 | const somethings_wrong_timeout = 10000; 315 | const startTime = Date.now(); 316 | let started_okay = false; 317 | 318 | const onReadyPromise = this.lsp_client.onReady().then(() => { 319 | started_okay = true; 320 | }); 321 | 322 | try { 323 | while (true) { 324 | const elapsedTime = Date.now() - startTime; 325 | if (started_okay) { 326 | console.log(`${logts()} RUST /START after ${elapsedTime}ms`); 327 | break; 328 | } 329 | if (elapsedTime >= somethings_wrong_timeout) { 330 | throw new Error("timeout"); 331 | } 332 | console.log(`${logts()} RUST waiting...`); 333 | await new Promise(resolve => setTimeout(resolve, 100)); 334 | } 335 | } catch (e) { 336 | console.log(`${logts()} RUST START PROBLEM e=${e}`); 337 | this.lsp_dispose(); 338 | return; 339 | } 340 | 341 | let success = await this.ping(); 342 | if (!success) { 343 | console.log("RUST ping failed"); 344 | this.lsp_dispose(); 345 | return; 346 | } 347 | // At this point we had successful client_info and workspace_folders server to client calls, 348 | // therefore the LSP server is started. 349 | // A little doubt remains about the http port, but it's very likely there's no race. 350 | await this.read_caps(); 351 | await this.fetch_toolbox_config(); 352 | } 353 | 354 | public async start_lsp_socket() { 355 | console.log("RUST start_lsp_socket"); 356 | this.lsp_socket = new net.Socket(); 357 | this.lsp_socket.on('error', (error) => { 358 | console.log("RUST socket error"); 359 | console.log(error); 360 | console.log("RUST /error"); 361 | this.lsp_dispose(); 362 | }); 363 | this.lsp_socket.on('close', () => { 364 | console.log("RUST socket closed"); 365 | this.lsp_dispose(); 366 | }); 367 | this.lsp_socket.on('connect', async () => { 368 | console.log("RUST LSP socket connected"); 369 | this.lsp_client = new lspClient.LanguageClient( 370 | 'Custom rust LSP server', 371 | async () => { 372 | if (this.lsp_socket === undefined) { 373 | return Promise.reject("this.lsp_socket is undefined, that should not happen"); 374 | } 375 | return Promise.resolve({ 376 | reader: this.lsp_socket, 377 | writer: this.lsp_socket 378 | }); 379 | }, 380 | this.lsp_client_options 381 | ); 382 | // client.registerProposedFeatures(); 383 | this.lsp_disposable = this.lsp_client.start(); 384 | console.log(`RUST DEBUG START`); 385 | try { 386 | await this.lsp_client.onReady(); 387 | console.log(`RUST DEBUG /START`); 388 | } catch (e) { 389 | console.log(`RUST DEBUG START PROBLEM e=${e}`); 390 | } 391 | }); 392 | this.lsp_socket.connect(DEBUG_LSP_PORT); 393 | } 394 | 395 | async rag_status() { 396 | try { 397 | let url = this.rust_url(); 398 | if (!url) { 399 | return Promise.reject("rag status no rust binary working, very strange"); 400 | } 401 | url += "v1/rag-status"; 402 | let req = new fetchH2.Request(url, { 403 | method: "GET", 404 | redirect: "follow", 405 | cache: "no-cache", 406 | referrer: "no-referrer", 407 | }); 408 | let resp = await fetchH2.fetch(req, { timeout: 5000 }); 409 | if (resp.status !== 200) { 410 | console.log(["rag status http status", resp.status]); 411 | return Promise.reject("rag status bad status"); 412 | } 413 | let rag_status = await resp.json(); 414 | return rag_status; 415 | } catch (e) { 416 | console.log(["rag status error:", e]); 417 | } 418 | return false; 419 | } 420 | 421 | async fetch_toolbox_config(): Promise { 422 | const rust_url = this.rust_url(); 423 | 424 | if (!rust_url) { 425 | console.log(["fetch_toolbox_config: No rust binary working"]); 426 | return Promise.reject("No rust binary working"); 427 | } 428 | const url = rust_url + "v1/customization"; 429 | 430 | const request = new fetchH2.Request(url, { method: "GET" }); 431 | 432 | const response = await fetchH2.fetch(request, { timeout: 5000 }); 433 | 434 | if (!response.ok) { 435 | console.log([ 436 | "fetch_toolbox_config: Error fetching toolbox config", 437 | response.status, 438 | url, 439 | ]); 440 | return Promise.reject( 441 | `Error fetching toolbox config: [status: ${response.status}] [statusText: ${response.statusText}]` 442 | ); 443 | } 444 | 445 | // TBD: type-guards or some sort of runtime validation 446 | const json = await response.json() as ToolboxConfig; 447 | console.log(["success fetch_toolbox_config", json]); 448 | 449 | global.toolbox_config = json; 450 | await register_commands(); 451 | return json; 452 | } 453 | } 454 | 455 | export type ChatMessageFromLsp = { 456 | role: string; 457 | content: string; 458 | }; 459 | 460 | export type ToolboxCommand = { 461 | description: string; 462 | messages: ChatMessageFromLsp[]; 463 | selection_needed: number[]; 464 | selection_unwanted: boolean; 465 | insert_at_cursor: boolean; 466 | }; 467 | 468 | export type SystemPrompt = { 469 | description: string; 470 | text: string; 471 | }; 472 | 473 | export type ToolboxConfig = { 474 | system_prompts: Record; 475 | toolbox_commands: Record; 476 | }; 477 | 478 | function logts() { 479 | const now = new Date(); 480 | const hours = String(now.getHours()).padStart(2, '0'); 481 | const minutes = String(now.getMinutes()).padStart(2, '0'); 482 | const seconds = String(now.getSeconds()).padStart(2, '0'); 483 | const milliseconds = String(now.getMilliseconds()).padStart(3, '0'); 484 | return `${hours}${minutes}${seconds}.${milliseconds}`; 485 | } 486 | -------------------------------------------------------------------------------- /src/quickProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { ToolboxCommand } from "./launchRust"; 4 | import { 5 | setInputValue, 6 | } from "refact-chat-js/dist/events"; 7 | 8 | type PlainTextMessage = { 9 | role: 'system'; 10 | content: string; 11 | }; 12 | 13 | type ChatMessage = PlainTextMessage; 14 | 15 | export class QuickActionProvider implements vscode.CodeActionProvider { 16 | private static actions: vscode.CodeAction[] = []; 17 | private static quickActionDisposables: vscode.Disposable[] = []; 18 | 19 | public static readonly providedCodeActionKinds = [ 20 | vscode.CodeActionKind.QuickFix, 21 | vscode.CodeActionKind.RefactorRewrite 22 | ]; 23 | 24 | provideCodeActions( 25 | document: vscode.TextDocument, 26 | range: vscode.Range | vscode.Selection, 27 | context: vscode.CodeActionContext, 28 | token: vscode.CancellationToken 29 | ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { 30 | // Create new instances of Refact.ai actions 31 | const refactActions = QuickActionProvider.actions.map(action => { 32 | const newAction = new vscode.CodeAction(action.title, action.kind); 33 | if (action.command) { 34 | const diagnosticRange = context.diagnostics[0]?.range || range; 35 | 36 | newAction.command = { 37 | ...action.command, 38 | arguments: [ 39 | action.command.arguments?.[0], 40 | action.command.arguments?.[1], 41 | { 42 | range: diagnosticRange, 43 | diagnostics: context.diagnostics 44 | } 45 | ] 46 | }; 47 | } 48 | return newAction; 49 | }); 50 | 51 | return refactActions; 52 | } 53 | 54 | public static updateActions = async (toolboxCommands: Record) => { 55 | this.actions = Object.entries(toolboxCommands).map(([id, command]) => { 56 | if(id === 'help') { return; } 57 | let action; 58 | if(id === 'bugs') { 59 | action = new vscode.CodeAction('Refact.ai: ' + command.description, vscode.CodeActionKind.QuickFix); 60 | } else { 61 | action = new vscode.CodeAction('Refact.ai: ' + command.description, vscode.CodeActionKind.RefactorRewrite); 62 | } 63 | action.command = { 64 | command: 'refactcmd.' + id, 65 | title: 'Refact.ai: ' + command.description, 66 | arguments: [id, command] 67 | }; 68 | return action; 69 | }).filter((action): action is vscode.CodeAction => action !== undefined); 70 | 71 | const dispose = (disposables: vscode.Disposable[]) => { 72 | disposables.forEach(d => d.dispose()); 73 | }; 74 | 75 | dispose(this.quickActionDisposables); 76 | 77 | this.actions.forEach(action => { 78 | if (action.command) { 79 | try { 80 | // XXX: this returns disposable, we need to dispose of old before setting new 81 | let disposable = vscode.commands.registerCommand( 82 | action.command.command, 83 | (actionId: string, command: ToolboxCommand, context?: { range: vscode.Range, diagnostics: vscode.Diagnostic[] }) => { 84 | QuickActionProvider.handleAction(actionId, command, context); 85 | }, 86 | ); 87 | this.quickActionDisposables.push(disposable); 88 | } catch (e) { 89 | console.error('Error registering command', e); 90 | } 91 | } 92 | }); 93 | }; 94 | 95 | public static sendQuickActionToChat(messageBlock: string) { 96 | if (!global || !global.side_panel || !global.side_panel._view) { 97 | return; 98 | } 99 | const message = setInputValue({ 100 | value: messageBlock, 101 | // eslint-disable-next-line @typescript-eslint/naming-convention 102 | send_immediately: true 103 | }); 104 | global.side_panel._view.webview.postMessage(message); 105 | } 106 | 107 | public static async handleAction(actionId: string, command: ToolboxCommand, context?: { range: vscode.Range, diagnostics: vscode.Diagnostic[] }) { 108 | const editor = vscode.window.activeTextEditor; 109 | if (!editor) { 110 | vscode.window.showErrorMessage('No active editor'); 111 | return; 112 | } 113 | 114 | const filePath = vscode.window.activeTextEditor?.document.fileName || ""; 115 | const workspaceFolders = vscode.workspace.workspaceFolders; 116 | let relativePath: string = ""; 117 | 118 | if (workspaceFolders) { 119 | const workspacePath = workspaceFolders[0].uri.fsPath; 120 | relativePath = path.relative(workspacePath, filePath); 121 | } 122 | 123 | const selection = editor.selection; 124 | let codeSnippet = ''; 125 | let middleLineOfSelection = 0; 126 | let diagnosticMessage = ''; 127 | 128 | // if no diagnostic were present, taking user's selection instead 129 | if (actionId === 'bugs' && context?.diagnostics && context.diagnostics.length > 0) { 130 | const diagnostic = context.diagnostics[0]; 131 | diagnosticMessage = diagnostic.message; 132 | middleLineOfSelection = diagnostic.range.start.line; 133 | 134 | codeSnippet = editor.document.getText(diagnostic.range); 135 | } else { 136 | codeSnippet = editor.document.getText(selection); 137 | middleLineOfSelection = Math.floor((selection.start.line + selection.end.line) / 2); 138 | } 139 | 140 | if (!codeSnippet) { 141 | const cursorPosition = selection.isEmpty ? editor.selection.active : selection.start; 142 | const startLine = Math.max(0, cursorPosition.line - 2); 143 | const endLine = Math.min(editor.document.lineCount - 1, cursorPosition.line + 2); 144 | const range = new vscode.Range(startLine, 0, endLine, editor.document.lineAt(endLine).text.length); 145 | codeSnippet = editor.document.getText(range); 146 | middleLineOfSelection = cursorPosition.line; 147 | } 148 | 149 | const messageBlock = command.messages.map(({content}) => ( 150 | content 151 | // we should fetch default prompt somehow 152 | .replace("%PROMPT_DEFAULT%", '') 153 | .replace("%CURRENT_FILE_PATH_COLON_CURSOR%", '') 154 | .replace("%CURRENT_FILE%", filePath) 155 | .replace("%CURSOR_LINE%", (middleLineOfSelection + 1).toString()) 156 | .replace("%CODE_SELECTION%", codeSnippet + "\n") 157 | )).join("\n"); 158 | 159 | vscode.commands.executeCommand("refactaicmd.callChat"); 160 | this.sendQuickActionToChat(messageBlock); 161 | } 162 | } 163 | 164 | export const updateQuickActions = QuickActionProvider.updateActions; -------------------------------------------------------------------------------- /src/rconsoleCommands.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as fetchAPI from "./fetchAPI"; 4 | import * as chatTab from "./chatTab"; 5 | import * as estate from "./estate"; 6 | 7 | 8 | export type ThreadCallback = (role: string, answer: string) => void; 9 | export type Messages = [string, string][]; 10 | export type ThreadEndCallback = (messages: Messages, last_affected_line?: number) => void; 11 | 12 | 13 | export function createCommandName(command: string): string { 14 | return `run_rconsole_command_${command}`; 15 | } 16 | 17 | function similarity_score(a: string, b: string): number { 18 | let score = 0; 19 | let digrams1 = get_digrams(a); 20 | let digrams2 = get_digrams(b); 21 | let chars1 = get_chars(a); 22 | let chars2 = get_chars(b); 23 | digrams1 = new Set([...digrams1, ...chars1]); 24 | digrams2 = new Set([...digrams2, ...chars2]); 25 | let intersection = new Set([...digrams1].filter(x => digrams2.has(x))); 26 | let union = new Set([...digrams1, ...digrams2]); 27 | score = intersection.size / union.size; 28 | return score; 29 | } 30 | 31 | function get_digrams(str: string): Set 32 | { 33 | let digrams = new Set(); 34 | for (let i = 0; i < str.length - 1; i++) { 35 | let digram = str.substring(i, i + 2); 36 | digrams.add(digram); 37 | } 38 | return digrams; 39 | } 40 | 41 | function get_chars(str: string): Set 42 | { 43 | let chars = new Set(); 44 | for (let i = 0; i < str.length; i++) { 45 | let char = str.substring(i, i + 1); 46 | chars.add(char); 47 | } 48 | return chars; 49 | } 50 | 51 | export async function get_hints( 52 | msgs: Messages, 53 | unfinished_text: string, 54 | selected_range: vscode.Range, 55 | model_name: string, 56 | ): Promise<[string, string, string]> { 57 | 58 | const toolbox_config = await ensure_toolbox_config(); 59 | let commands_available = toolbox_config?.toolbox_commands; 60 | 61 | if (unfinished_text.startsWith("/") && commands_available) { 62 | let cmd_score: { [key: string]: number } = {}; 63 | let unfinished_text_up_to_space = unfinished_text.split(" ")[0]; 64 | 65 | // handle help 66 | let result = ""; 67 | if (unfinished_text_up_to_space === "/help") { 68 | let all_cmds_sorted = Object.getOwnPropertyNames(commands_available).sort(); 69 | result += "
"; 70 | for (let i = 0; i < all_cmds_sorted.length; i++) { 71 | let cmd = all_cmds_sorted[i]; 72 | if (cmd === "help") { 73 | continue; 74 | } 75 | let text = commands_available[cmd].description || ""; 76 | if (i === Math.floor((all_cmds_sorted.length + 1) / 2)) { 77 | result += "\n"; 78 | } 79 | result += `/${cmd} ${text}
\n`; 80 | } 81 | result += "
\n"; 82 | result += "\n[Customize toolbox](command:refactaicmd.openPromptCustomizationPage)\n"; 83 | return [result, "Available commands:", ""]; 84 | } else { 85 | for (let cmd in commands_available) { 86 | let text = commands_available[cmd].description || ""; 87 | let ideal = "/" + cmd + " "; 88 | if (unfinished_text.startsWith(ideal)) { 89 | result += "/" + cmd + " " + text + "

\n"; 90 | let selection_unwanted = commands_available[cmd]["selection_unwanted"]; 91 | let selection_needed = commands_available[cmd]["selection_needed"]; 92 | if (selection_needed.length > 0) { 93 | result += `Selection needed: ${selection_needed[0]}..${selection_needed[1]} lines
\n`; 94 | } 95 | if (selection_unwanted) { 96 | result += `This function works with no lines selected
\n`; 97 | } 98 | return [result, "Available commands:", cmd]; 99 | } 100 | let score1 = similarity_score(unfinished_text_up_to_space, "/" + cmd + " " + text); 101 | let score2 = similarity_score(unfinished_text_up_to_space, "/" + cmd); 102 | cmd_score[cmd] = Math.max(score1, score2); 103 | } 104 | let sorted_cmd_score = Object.entries(cmd_score).sort((a, b) => b[1] - a[1]); 105 | let top3 = sorted_cmd_score.slice(0, 3); 106 | for (let i = 0; i < top3.length; i++) { 107 | let cmd = top3[i][0]; 108 | // const cmd_name = createCommandName(cmd); 109 | // result += `[**/${cmd}** ${text}](command:${cmd_name})
\n`; 110 | let text = commands_available[cmd].description || ""; 111 | result += `/${cmd} ${text}
\n`; 112 | } 113 | // TODO: find how to link to a file 114 | result += "\n[Customize toolbox](command:refactaicmd.openPromptCustomizationPage)\n"; 115 | return [result, "Available commands:", top3[0][0]]; 116 | } 117 | } else { 118 | if (!selected_range.isEmpty) { 119 | let lines_n = selected_range.end.line - selected_range.start.line + 1; 120 | return [ 121 | `Ask any question about these ${lines_n} lines. Try "explain this" or commands starting with \"/\", for example "/help".\n\n` + 122 | `Model: ${model_name}\n`, 123 | "🪄 Selected text", ""]; 124 | } else { 125 | return [ 126 | `Any question about this source file. To generate new code, use \"/gen\", try other commands starting with \"/\", for example "/help".\n\n` + 127 | `Model: ${model_name}\n`, 128 | "🪄 This File", ""]; 129 | } 130 | } 131 | } 132 | 133 | export function initial_messages(working_on_attach_filename: string, selection: vscode.Selection) 134 | { 135 | // NOTE: this is initial messages for a chat without a command. With command it will get the structure from the command. 136 | let messages: Messages = []; 137 | if (!working_on_attach_filename) { 138 | // this should not happen, because we started from a file 139 | return messages; 140 | } 141 | 142 | messages.push([ 143 | "user", 144 | `@file ${working_on_attach_filename}:${selection.anchor.line}` 145 | ]); 146 | 147 | return messages; 148 | } 149 | 150 | export async function stream_chat_without_visible_chat( 151 | messages: Messages, 152 | model_name: string, 153 | editor: vscode.TextEditor, 154 | selected_range: vscode.Range, 155 | cancelToken: vscode.CancellationToken, 156 | thread_callback: ThreadCallback, 157 | end_thread_callback: ThreadEndCallback, 158 | ) { 159 | let state = estate.state_of_editor(editor, "invisible_chat"); 160 | if (!state) { 161 | console.log("stream_chat_without_visible_chat: no state found"); 162 | return; 163 | } 164 | state.showing_diff_for_range = selected_range; 165 | await estate.switch_mode(state, estate.Mode.DiffWait); 166 | // Don't need anymore: user is already entertained 167 | // interactiveDiff.animation_start(editor, state); // this is an async function, active until the state is still DiffWait 168 | 169 | let answer = ""; 170 | let answer_role = ""; 171 | async function _streaming_callback(json: any) 172 | { 173 | if (typeof json !== "object") { 174 | return; 175 | } 176 | if (cancelToken.isCancellationRequested) { 177 | return; 178 | } else { 179 | let delta = ""; 180 | let role0, content0; 181 | if (json["choices"]) { 182 | let choice0 = json["choices"][0]; 183 | if (choice0["delta"]) { 184 | role0 = choice0["delta"]["role"]; 185 | content0 = choice0["delta"]["content"]; 186 | } else if (choice0["message"]) { 187 | role0 = choice0["message"]["role"]; 188 | content0 = choice0["message"]["content"]; 189 | } 190 | if (role0 === "context_file") { 191 | let file_dict = JSON.parse(content0); 192 | let file_content = file_dict["file_content"]; 193 | file_content = escape(file_content); 194 | delta += `📎 ${file_dict["file_name"]}
\n`; 195 | } else { 196 | delta = content0; 197 | } 198 | } 199 | if (delta) { 200 | answer += delta; 201 | } 202 | if (role0) { 203 | answer_role = role0; 204 | } 205 | if(answer_role && answer) { 206 | thread_callback(answer_role, answer); 207 | } 208 | } 209 | } 210 | 211 | async function _streaming_end_callback(error_message: string) 212 | { 213 | console.log("streaming end callback, error: " + error_message); 214 | if (!error_message) { 215 | messages.push([answer_role, answer]); 216 | let answer_by_backquote = answer.split("```"); 217 | let code_blocks = []; 218 | for (let i=1; i largest_block.length) { 224 | largest_block = block; 225 | } 226 | } 227 | 228 | if (largest_block) { 229 | let last_affected_line = chatTab.diff_paste_back( 230 | editor.document, 231 | selected_range, 232 | largest_block, 233 | ); 234 | end_thread_callback(messages, last_affected_line); 235 | } else { 236 | let state = estate.state_of_document(editor.document); 237 | if (state) { 238 | await estate.switch_mode(state, estate.Mode.Normal); 239 | } 240 | end_thread_callback(messages); 241 | } 242 | } else { 243 | let error_message_escaped = error_message.replace(//g, ">"); 244 | messages.push(["error", "When sending the actual request, an error occurred:\n\n" + error_message_escaped]); 245 | end_thread_callback(messages); 246 | let state = estate.state_of_editor(editor, "streaming_end_callback"); 247 | if (state) { 248 | await estate.switch_mode(state, estate.Mode.Normal); 249 | } 250 | } 251 | } 252 | 253 | let request = new fetchAPI.PendingRequest(undefined, cancelToken); 254 | request.set_streaming_callback(_streaming_callback, _streaming_end_callback); 255 | let third_party = true; // FIXME 256 | request.supply_stream(...fetchAPI.fetch_chat_promise( 257 | cancelToken, 258 | "chat-tab", 259 | messages, 260 | model_name, 261 | third_party, 262 | )); 263 | } 264 | 265 | 266 | async function _run_command(cmd: string, args: string, doc_uri: string, model_name: string, update_thread_callback: ThreadCallback, end_thread_callback: ThreadEndCallback) 267 | { 268 | const toolbox_config = await ensure_toolbox_config(); 269 | if(!toolbox_config) { 270 | console.log(["_run_command: no toolbox config found", doc_uri]); 271 | return; 272 | } 273 | const cmd_dict = toolbox_config?.toolbox_commands[cmd]; 274 | // let text = toolbox_config?.toolbox_commands[cmd].description ?? ""; 275 | let editor = vscode.window.visibleTextEditors.find((e) => { 276 | return e.document.uri.toString() === doc_uri; 277 | }); 278 | if (!editor) { 279 | console.log("_run_command: no editor found for " + doc_uri); 280 | let editor2 = vscode.window.visibleTextEditors.find((e) => { 281 | return e.document.uri.toString() === doc_uri; 282 | }); 283 | console.log("_run_command: editor2", editor2); 284 | return; 285 | } 286 | 287 | let [official_selection1, _attach_range1, _working_on_attach_code, working_on_attach_filename, code_snippet] = chatTab.attach_code_from_editor(editor, false); 288 | let middle_line_of_selection = Math.floor((official_selection1.start.line + official_selection1.end.line) / 2); 289 | 290 | let selection_empty = official_selection1.isEmpty; 291 | let selection_unwanted = cmd_dict["selection_unwanted"]; 292 | let selection_needed = cmd_dict["selection_needed"]; 293 | if (selection_unwanted && !selection_empty) { 294 | return; 295 | } 296 | if (selection_needed) { 297 | let [smin, smax] = selection_needed; 298 | let official_selection_nlines = official_selection1.end.line - official_selection1.start.line; 299 | if (!selection_empty && official_selection_nlines === 0) { 300 | official_selection_nlines = 1; 301 | } 302 | if (official_selection_nlines < smin || official_selection_nlines > smax) { 303 | return; 304 | } 305 | } 306 | 307 | const messages: [string, string][] = []; 308 | let cmd_messages = cmd_dict["messages"]; 309 | let CURRENT_FILE_PATH_COLON_CURSOR = `${working_on_attach_filename}:${middle_line_of_selection + 1}`; 310 | // let CURRENT_FILE = editor.document.uri.fsPath; 311 | console.log("CURRENT_FILE_PATH_COLON_CURSOR", CURRENT_FILE_PATH_COLON_CURSOR); 312 | for (let i=0; i { 338 | global.toolbox_command_disposables.forEach(disposable => disposable.dispose()); 339 | global.toolbox_command_disposables = []; 340 | try { 341 | 342 | const toolbox_config = await ensure_toolbox_config(); 343 | const commands_available = toolbox_config?.toolbox_commands; 344 | 345 | for (let cmd in commands_available) { 346 | let d = vscode.commands.registerCommand('refactaicmd.cmd_' + cmd, 347 | async (args, doc_uri, model_name: string, update_thread_callback: ThreadCallback, end_thread_callback: ThreadEndCallback) => { 348 | if (!model_name) { 349 | [model_name,] = await chatTab.chat_model_get(); 350 | } 351 | _run_command(cmd, args, doc_uri, model_name, update_thread_callback, end_thread_callback); 352 | } 353 | ); 354 | global.toolbox_command_disposables.push(d); 355 | } 356 | } catch (e) { 357 | console.log(["register_commands error"]); 358 | console.error(e); 359 | } 360 | } 361 | 362 | export async function ensure_toolbox_config() { 363 | if (global.toolbox_config) { return global.toolbox_config; } 364 | return global.rust_binary_blob?.fetch_toolbox_config(); 365 | } 366 | -------------------------------------------------------------------------------- /src/rconsoleProvider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as rconsoleCommands from "./rconsoleCommands"; 4 | import * as sidebar from "./sidebar"; 5 | import * as chatTab from "./chatTab"; 6 | import { v4 as uuidv4 } from 'uuid'; 7 | import { ChatMessages } from 'refact-chat-js/dist/events'; 8 | 9 | export class MyCommentAuthorInformation implements vscode.CommentAuthorInformation { 10 | name: string; 11 | iconPath?: vscode.Uri; 12 | constructor(name: string, iconPath?: vscode.Uri) { 13 | this.name = name; 14 | this.iconPath = iconPath; 15 | } 16 | } 17 | export class MyComment implements vscode.Comment { 18 | body: vscode.MarkdownString; 19 | mode: vscode.CommentMode; 20 | author: vscode.CommentAuthorInformation; 21 | reactions?: vscode.CommentReaction[]; 22 | 23 | constructor(body: string, mode: vscode.CommentMode, author: vscode.CommentAuthorInformation) { 24 | this.body = new vscode.MarkdownString(); 25 | 26 | this.body.appendMarkdown(body); 27 | 28 | this.body.isTrusted = true; 29 | 30 | this.body.supportHtml = true; 31 | this.mode = mode; 32 | this.author = author; 33 | this.reactions = undefined; 34 | // this.reactions = [ 35 | // new MyCommentReaction("Like", vscode.Uri.parse("https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png"), 15, false), 36 | // // new vscode.CommentReaction('👎', 'Dislike'), 37 | // ]; 38 | } 39 | } 40 | 41 | export class RefactConsoleProvider { 42 | comment_controller: vscode.CommentController; 43 | official_selection: vscode.Range; 44 | attach_range: vscode.Range; 45 | working_on_attach_code: string; 46 | working_on_attach_filename: string; 47 | code_snippet: string = ""; 48 | editor: vscode.TextEditor; 49 | model_name: string; 50 | thread: vscode.CommentThread; 51 | messages: rconsoleCommands.Messages; 52 | 53 | hint_debounce?: NodeJS.Timeout; 54 | input_text = ""; 55 | hint_mode = true; 56 | disposable_commands: vscode.Disposable[] = []; 57 | 58 | 59 | static close_all_consoles(): vscode.Uri | undefined { 60 | if (global.comment_disposables) { 61 | for (let d of global.comment_disposables) { 62 | d.dispose(); 63 | } 64 | } 65 | global.comment_disposables = []; 66 | let ret = global.comment_file_uri; 67 | global.comment_file_uri = undefined; 68 | return ret; 69 | } 70 | 71 | static async open_between_lines(editor: vscode.TextEditor) { 72 | let [model_name, _] = await chatTab.chat_model_get(); 73 | return new RefactConsoleProvider(editor, model_name); 74 | } 75 | 76 | constructor( 77 | editor: vscode.TextEditor, 78 | model_name: string, 79 | ) { 80 | RefactConsoleProvider.close_all_consoles(); 81 | 82 | this.editor = editor; 83 | this.model_name = model_name; 84 | this.comment_controller = vscode.comments.createCommentController("refactai-test", "RefactAI Test Comments"); 85 | global.comment_file_uri = editor.document.uri; 86 | 87 | [ 88 | this.official_selection, 89 | this.attach_range, 90 | this.working_on_attach_code, 91 | this.working_on_attach_filename, 92 | this.code_snippet 93 | ] = chatTab.attach_code_from_editor(editor, false); 94 | 95 | this.dispose = this.dispose.bind(this); 96 | this.handle_message_stream = this.handle_message_stream.bind(this); 97 | this.handle_message_stream_end = this.handle_message_stream_end.bind(this); 98 | this.handle_text_document_change = this.handle_text_document_change.bind(this); 99 | this.handle_close_inline_chat = this.handle_close_inline_chat.bind(this); 100 | this.handle_move_chat_to_sidebar = this.handle_move_chat_to_sidebar.bind(this); 101 | 102 | this.thread = this.initialize_thread(); 103 | this.messages = this.initial_messages(); 104 | 105 | global.comment_disposables.push(this); 106 | global.comment_disposables.push( 107 | vscode.workspace.onDidChangeTextDocument(this.handle_text_document_change) 108 | ); 109 | global.comment_disposables.push( 110 | vscode.workspace.onDidCloseTextDocument(this.handle_text_document_close) 111 | ); 112 | 113 | global.comment_disposables.push( 114 | vscode.commands.registerCommand("refactaicmd.sendChatToSidebar", this.handle_move_chat_to_sidebar) 115 | ); 116 | 117 | global.comment_disposables.push( 118 | vscode.commands.registerCommand("refactaicmd.closeInlineChat", this.handle_close_inline_chat) 119 | ); 120 | 121 | this.scroll_to_thread(); 122 | 123 | this.initial_message(); 124 | 125 | } 126 | 127 | async initial_message() { 128 | await vscode.commands.executeCommand('setContext', 'refactcx.runEsc', true); 129 | // This trick puts cursor into the input box, possibly VS thinks the only use for 130 | // the thread is to ask user if there are no messages. But then we add a message. 131 | await new Promise(resolve => setTimeout(resolve, 100)); 132 | let [hint, author, _top1] = await rconsoleCommands.get_hints(this.messages, "", this.official_selection, this.model_name); 133 | const hint_comment = this.format_message(author, hint); 134 | this.thread.comments = [hint_comment]; 135 | } 136 | 137 | async handle_move_chat_to_sidebar() { 138 | let question = "```\n" + this.code_snippet + "\n```\n\n" + this.input_text; 139 | this.activate_chat(this.messages, question); 140 | } 141 | 142 | handle_close_inline_chat() { 143 | global.comment_disposables.forEach(disposable => { 144 | disposable.dispose(); 145 | }); 146 | global.comment_disposables = []; 147 | } 148 | 149 | remove_click_handlers_for_commands() { 150 | this.disposable_commands.forEach(command => command.dispose()); 151 | } 152 | 153 | async add_click_handlers_for_commands() { 154 | this.remove_click_handlers_for_commands(); 155 | const toolbox_config = await rconsoleCommands.ensure_toolbox_config(); 156 | if(!toolbox_config) { 157 | console.log(["RefactConsoleCommands: No toolbox config found"]); 158 | return; 159 | } 160 | Object.keys(toolbox_config.toolbox_commands).forEach(cmd => { 161 | if (cmd !== "help") { 162 | const commandName = rconsoleCommands.createCommandName(cmd); 163 | this.disposable_commands.push( 164 | vscode.commands.registerCommand(commandName, () => { 165 | this.activate_cmd(cmd, ""); 166 | }) 167 | ); 168 | } 169 | }); 170 | }; 171 | 172 | reset_thread() { 173 | this.thread.comments = [ 174 | this.format_message("assistant", "Thinking...") 175 | ]; 176 | } 177 | 178 | send_messages_to_thread() { 179 | const assistant_messages = this.messages.filter(message => message[0] !== "context_file" && message[0] !== "user"); 180 | const last_message = assistant_messages.slice(-1); 181 | let new_comments = last_message.map(([author, message]) => this.format_message(author, message)); 182 | 183 | if (new_comments.length === 0) { 184 | this.reset_thread(); 185 | } else { 186 | this.thread.comments = new_comments; 187 | } 188 | } 189 | 190 | format_message(author: string, text: string) { 191 | let embellished_author = author; 192 | if (author === "user") { 193 | embellished_author = "You"; 194 | } 195 | if (author === "assistant") { 196 | embellished_author = "🤖 Refact"; 197 | } 198 | if (author === "error") { 199 | embellished_author = "🤖 Snap!"; 200 | } 201 | const comment_author_info = new MyCommentAuthorInformation(embellished_author); 202 | return new MyComment(text, vscode.CommentMode.Preview, comment_author_info); 203 | } 204 | 205 | initial_messages(): rconsoleCommands.Messages { 206 | return rconsoleCommands.initial_messages(this.working_on_attach_filename, this.editor.selection); 207 | } 208 | 209 | dispose() { 210 | console.log("console dispose"); 211 | this.remove_click_handlers_for_commands(); 212 | // this.thread.dispose(); 213 | this.comment_controller.dispose(); 214 | } 215 | 216 | initialize_thread(): vscode.CommentThread { 217 | const next_line = this.official_selection.isEmpty ? ( 218 | this.official_selection.end.line 219 | ) : Math.min( 220 | this.official_selection.end.line + 1, 221 | this.editor.document.lineCount - 1 222 | ); 223 | 224 | const comment_thread_range = new vscode.Range(next_line, 0, next_line, 0); 225 | this.comment_controller.commentingRangeProvider = { 226 | provideCommentingRanges: (document: vscode.TextDocument) => { 227 | return [comment_thread_range]; 228 | }, 229 | }; 230 | this.comment_controller.reactionHandler = (comment: vscode.Comment, reaction: vscode.CommentReaction): Thenable => { 231 | console.log("reactionHandler", comment, reaction); 232 | return Promise.resolve(); 233 | }; 234 | 235 | const thread = this.comment_controller.createCommentThread( 236 | this.editor.document.uri, 237 | comment_thread_range, 238 | [], 239 | ); 240 | 241 | thread.canReply = true; 242 | thread.label = "Refact Console (F1)"; 243 | thread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded; 244 | 245 | return thread; 246 | } 247 | 248 | handle_message_stream(author_role: string, answer: string) { 249 | const last = this.thread.comments.length > 0 ? this.thread.comments[this.thread.comments.length - 1] : null; 250 | const comment = this.format_message(author_role, answer); 251 | 252 | if(last instanceof MyComment && last.author.name === comment.author.name) { 253 | const previousComments = this.thread.comments.slice(0, -1); 254 | this.thread.comments = [ 255 | ...previousComments, 256 | comment 257 | ]; 258 | } else { 259 | this.thread.comments = [ 260 | ...this.thread.comments, 261 | comment, 262 | ]; 263 | } 264 | }; 265 | 266 | scroll_to_thread() { 267 | return; 268 | } 269 | 270 | send_thread_to_line(line: number) { 271 | const thread_range = new vscode.Range(line, 0, line, 0); 272 | this.thread.range = thread_range; 273 | this.scroll_to_thread(); 274 | } 275 | 276 | handle_message_stream_end(response_messages: rconsoleCommands.Messages, maybe_last_affected_line? : number) { 277 | this.messages = response_messages; 278 | if( 279 | this.thread && 280 | this.thread.range && 281 | maybe_last_affected_line !== undefined 282 | && maybe_last_affected_line >= 0 283 | && this.thread.range.start.line !== maybe_last_affected_line 284 | ) { 285 | this.send_thread_to_line(maybe_last_affected_line); 286 | } 287 | 288 | this.send_messages_to_thread(); 289 | vscode.commands.executeCommand("setContext", "refactaicmd.openSidebarButtonEnabled", true); 290 | // "comments/commentThread/additionalActions": [ 291 | // { 292 | // "command": "refactaicmd.sendChatToSidebar", 293 | // "group": "inline@1" 294 | // }, 295 | // { 296 | // "command": "refactaicmd.closeInlineChat", 297 | // "group": "inline@2" 298 | // } 299 | // ] 300 | } 301 | 302 | handle_text_document_close(e: vscode.TextDocument) { 303 | if (e.uri.scheme !== "comment") { 304 | return; 305 | } 306 | RefactConsoleProvider.close_all_consoles(); 307 | } 308 | 309 | 310 | async handle_user_pressed_enter(event: vscode.TextDocumentChangeEvent) { 311 | // handle pressed enter 312 | // active chat also close the console 313 | const toolbox_config = await rconsoleCommands.ensure_toolbox_config(); 314 | 315 | let comment_editor = vscode.window.visibleTextEditors.find((e1) => { 316 | return e1.document.uri === event.document.uri; 317 | }); 318 | 319 | let first_line = this.input_text.split("\n")[0]; 320 | 321 | if (first_line.startsWith("/") && toolbox_config) { 322 | for (let cmd in toolbox_config.toolbox_commands) { 323 | if (cmd === "help") { 324 | continue; 325 | } 326 | if (first_line.startsWith("/" + cmd)) { // maybe first_line.trim() === `/${cmd}` 327 | this.hint_mode = false; 328 | vscode.commands.executeCommand("setContext", "refactaicmd.openSidebarButtonEnabled", false); 329 | if (comment_editor) { 330 | await comment_editor.edit(edit => { 331 | edit.delete(new vscode.Range(0, 0, 1000, 0)); 332 | }); 333 | } 334 | this.reset_thread(); 335 | let args = first_line.substring(cmd.length + 1); 336 | this.activate_cmd(cmd, args.trim()); 337 | return; 338 | } 339 | } 340 | } else if (this.input_text.trim() === "") { 341 | // do nothing 342 | } else { 343 | this.hint_mode = false; 344 | if (this.code_snippet !== "") { 345 | const question = "```\n" + this.code_snippet + "\n```\n\n" + this.input_text; 346 | this.activate_chat(this.messages, question); 347 | } else { // if(this.messages.length > 1) 348 | this.activate_chat(this.messages, this.input_text); 349 | } 350 | } 351 | } 352 | 353 | async hints_and_magic_tabs(event: vscode.TextDocumentChangeEvent) { 354 | const [hint, author, top1] = await rconsoleCommands.get_hints(this.messages, this.input_text, this.official_selection, this.model_name); 355 | 356 | if (this.hint_mode) { 357 | if (this.hint_debounce) { 358 | clearTimeout(this.hint_debounce); 359 | } 360 | this.hint_debounce = setTimeout(async () => { 361 | // this is a heavy operation, changes the layout and lags the UI 362 | this.thread.comments = [ 363 | this.format_message(author, hint) 364 | ]; 365 | }, 200); 366 | } 367 | 368 | await vscode.commands.executeCommand('setContext', 'refactcx.runEsc', true); 369 | 370 | if (top1 && this.input_text.match(/\/[a-zA-Z0-9_]+[\t ]+$/)) { 371 | let comment_editor = vscode.window.visibleTextEditors.find((e1) => { 372 | return e1.document.uri === event.document.uri; 373 | }); 374 | let ideal = "/" + top1 + " "; 375 | if (comment_editor) { 376 | if (this.input_text.trim() !== ideal.trim()) { 377 | await comment_editor.edit(edit => { 378 | edit.delete(new vscode.Range(0, 0, 1000, 0)); 379 | edit.insert(new vscode.Position(0, 0), ideal); 380 | }); 381 | } 382 | } 383 | return; 384 | } 385 | } 386 | 387 | async handle_text_document_change(e: vscode.TextDocumentChangeEvent) { 388 | // console.log("onDidChangeTextDocument", e.document.uri, this.messages.length); 389 | 390 | if (e.document.uri.scheme !== "comment") { 391 | return; 392 | } 393 | 394 | this.input_text = e.document.getText(); 395 | // this.hint_mode = this.input_text.startsWith("/"); 396 | 397 | // if(this.hint_mode) { 398 | // this.add_click_handlers_for_commands(); 399 | // } else { 400 | // this.remove_click_handlers_for_commands(); 401 | // } 402 | 403 | 404 | if (this.input_text.includes("\n")) { 405 | return await this.handle_user_pressed_enter(e); 406 | } 407 | 408 | await this.hints_and_magic_tabs(e); 409 | } 410 | 411 | activate_cmd( 412 | cmd: string, 413 | args: string 414 | ) { 415 | console.log(`activate_cmd refactaicmd.cmd_${cmd} args="${args}"`); 416 | vscode.commands.executeCommand("setContext", "refactaicmd.runningChat", true); 417 | vscode.commands.executeCommand( 418 | "refactaicmd.cmd_" + cmd, 419 | args, 420 | this.editor.document.uri.toString(), 421 | this.model_name, 422 | this.handle_message_stream, // bind this 423 | this.handle_message_stream_end // bind this 424 | ); 425 | } 426 | 427 | async activate_chat( 428 | messages: rconsoleCommands.Messages, 429 | question: string, 430 | ) { 431 | console.log(`activate_chat question.length=${question.length}`); 432 | RefactConsoleProvider.close_all_consoles(); 433 | await vscode.commands.executeCommand("refactai-toolbox.focus"); 434 | for (let i = 0; i < 10; i++) { 435 | await new Promise((resolve) => setTimeout(resolve, 100)); 436 | if (global.side_panel && global.side_panel._view) { 437 | break; 438 | } 439 | } 440 | 441 | const id = uuidv4(); 442 | 443 | // const messagesWithQuestion: ChatMessages = appendQuestion(question, messages) as ChatMessages; 444 | 445 | // this really opens chat somehow 446 | let chat: chatTab.ChatTab | undefined = await sidebar.open_chat_tab( 447 | question, 448 | this.editor, 449 | false, 450 | this.model_name, 451 | [], 452 | // messagesWithQuestion, 453 | id, 454 | ); 455 | if (!chat) { 456 | return; 457 | } 458 | 459 | // TODO: This looks weird with all the changes from the call to restore 460 | // const disposables: vscode.Disposable[] = []; 461 | // disposables.push(chat.web_panel.webview.onDidReceiveMessage(e => { 462 | // if(chat && e.type === EVENT_NAMES_FROM_CHAT.READY && e.payload.id === chat.chat_id) { 463 | // chat.handleChatQuestion({ 464 | // id: chat.chat_id, 465 | // model: this.model_name, 466 | // title: question, 467 | // messages: messagesWithQuestion, 468 | // attach_file: false, 469 | // }); 470 | // disposables.forEach(d => d.dispose()); 471 | // } 472 | // })); 473 | 474 | await new Promise(r => setTimeout(r, 200)); 475 | } 476 | } 477 | 478 | function appendQuestion(question: string, messages: rconsoleCommands.Messages): rconsoleCommands.Messages { 479 | if(messages.length === 0) { 480 | return [["user", question]]; 481 | } 482 | const lastMessage = messages[messages.length - 1]; 483 | 484 | // for some reason this is circular? 485 | if(lastMessage[0] === "user") { 486 | const message: [string, string][] = [["user", lastMessage[1] + '\n' + question]]; 487 | return messages.slice(0, -1).concat(message); 488 | } 489 | 490 | return messages.concat([["user", question]]); 491 | } -------------------------------------------------------------------------------- /src/statisticTab.ts: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable @typescript-eslint/naming-convention */ 3 | // TODO: delete this file 4 | import * as vscode from "vscode"; 5 | // import { 6 | // EVENT_NAMES_TO_STATISTIC, 7 | // } from "refact-chat-js/dist/events"; 8 | import * as fetchAPI from "./fetchAPI"; 9 | import { v4 as uuidv4 } from "uuid"; 10 | 11 | export class StatisticTab { 12 | private _disposables: vscode.Disposable[] = []; 13 | public constructor( 14 | public web_panel: vscode.WebviewPanel | vscode.WebviewView, 15 | ) { 16 | this.handleEvents = this.handleEvents.bind(this); 17 | this.web_panel.webview.onDidReceiveMessage(this.handleEvents); 18 | } 19 | 20 | private handleEvents(message: any) { 21 | // switch (message.type) { 22 | // case EVENT_NAMES_TO_STATISTIC.REQUEST_STATISTIC_DATA: { 23 | // return fetchAPI 24 | // .get_statistic_data() 25 | // .then((data) => { 26 | // return this.web_panel.webview.postMessage({ 27 | // type: EVENT_NAMES_TO_STATISTIC.RECEIVE_STATISTIC_DATA, 28 | // payload: data, 29 | // }); 30 | // }) 31 | // .catch((err) => { 32 | // return this.web_panel.webview.postMessage({ 33 | // type: EVENT_NAMES_TO_STATISTIC.RECEIVE_STATISTIC_DATA_ERROR, 34 | // payload: { 35 | // message: err, 36 | // }, 37 | // }); 38 | // }); 39 | // } 40 | 41 | // } 42 | } 43 | 44 | dispose() { 45 | this._disposables.forEach((d) => d.dispose()); 46 | } 47 | 48 | public get_html_for_statistic( 49 | webview: vscode.Webview, 50 | extensionUri: any, 51 | isTab = false 52 | ): string { 53 | const nonce = uuidv4(); 54 | const scriptUri = webview.asWebviewUri( 55 | vscode.Uri.joinPath(extensionUri, "node_modules", "refact-chat-js", "dist", "chat", "index.umd.cjs") 56 | ); 57 | 58 | const styleMainUri = webview.asWebviewUri( 59 | vscode.Uri.joinPath(extensionUri, "node_modules", "refact-chat-js", "dist", "chat", "style.css") 60 | ); 61 | 62 | const styleOverride = webview.asWebviewUri( 63 | vscode.Uri.joinPath(extensionUri, "assets", "custom-theme.css") 64 | ); 65 | 66 | return ` 67 | 68 | 69 | 70 | 75 | 76 | 77 | 78 | Refact.ai Chat 79 | 80 | 81 | 82 | 83 |
84 | 85 | 86 | 92 | 93 | `; 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/statusBar.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as userLogin from "./userLogin"; 4 | import * as fetchH2 from 'fetch-h2'; 5 | import { RagStatus } from './fetchAPI'; 6 | 7 | 8 | let _website_message = ""; 9 | let _inference_message = ""; 10 | 11 | 12 | export function set_website_message(msg: string) 13 | { 14 | _website_message = msg; 15 | } 16 | 17 | 18 | export function set_inference_message(msg: string) 19 | { 20 | _inference_message = msg; 21 | } 22 | 23 | export class StatusBarMenu { 24 | menu: any = {}; 25 | socketerror: boolean = false; 26 | socketerror_msg: string = ''; 27 | spinner: boolean = false; 28 | ast_limit_hit: boolean = false; 29 | vecdb_limit_hit: boolean = false; 30 | vecdb_warning: string = ""; 31 | last_url: string = ""; 32 | last_model_name: string = ""; 33 | have_completion_success: boolean = false; 34 | access_level: number = -1; 35 | rag_status: string = ""; 36 | rag_tootip: string = ""; 37 | 38 | createStatusBarBlock(context: vscode.ExtensionContext) 39 | { 40 | const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); 41 | item.command = "refactaicmd.statusBarClick"; 42 | 43 | context.subscriptions.push(item); 44 | item.text = `$(codify-logo) Refact.ai`; 45 | item.tooltip = `Settings`; 46 | item.show(); 47 | 48 | this.menu = item; 49 | 50 | return this.menu; 51 | } 52 | 53 | choose_color() 54 | { 55 | if (this.access_level === 0) { 56 | this.menu.text = `$(refact-icon-privacy) Refact.ai`; 57 | this.menu.backgroundColor = undefined; 58 | this.menu.tooltip = `Refact can't access this file because of the privacy rules`; 59 | } else if (this.socketerror) { 60 | this.menu.text = `$(debug-disconnect) Refact.ai`; 61 | this.menu.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); 62 | if (this.socketerror_msg.indexOf("no model") !== -1) { 63 | this.menu.tooltip = `Either an outage on the server side, or your settings might be outdated:\n${this.socketerror_msg}`; 64 | } else { 65 | this.menu.tooltip = `Cannot reach the server:\n` + this.socketerror_msg; 66 | } 67 | } else if (!global.have_caps) { 68 | this.menu.text = `$(codify-logo) Refact.ai`; 69 | this.menu.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); 70 | let reach = global.rust_binary_blob ? global.rust_binary_blob.attemping_to_reach() : ""; 71 | this.menu.tooltip = `Inference server is currently unavailable\nAttempting to reach '${reach}'`; 72 | } else if (this.spinner) { 73 | this.menu.text = `$(sync~spin) Refact.ai`; 74 | this.menu.backgroundColor = undefined; 75 | } else if (this.ast_limit_hit) { 76 | this.menu.text = `$(debug-disconnect) AST files limit`; 77 | this.menu.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); 78 | this.menu.tooltip = "Click to make changes in settings"; 79 | } else if (this.vecdb_limit_hit) { 80 | this.menu.text = `$(debug-disconnect) VecDB files limit`; 81 | this.menu.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); 82 | this.menu.tooltip = "Click to make changes in settings"; 83 | } else if (this.vecdb_warning !== '') { 84 | this.menu.text = `$(debug-disconnect) Refact.ai`; 85 | this.menu.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); 86 | this.menu.tooltip = this.vecdb_warning; 87 | } else if (this.have_completion_success) { 88 | this.menu.text = this.rag_status || `$(codify-logo) Refact.ai`; 89 | this.menu.backgroundColor = undefined; 90 | let msg: string = ""; 91 | let reach = global.rust_binary_blob ? global.rust_binary_blob.attemping_to_reach() : ""; 92 | if (reach) { 93 | msg += `Communicating with:\n 🌩️ ${reach}`; 94 | } 95 | if (this.last_model_name) { 96 | if (msg) { 97 | msg += "\n\n"; 98 | } 99 | msg += `Last used model:\n 🧠 ${this.last_model_name}`; 100 | } 101 | if (this.rag_tootip) { 102 | if (msg) { 103 | msg += "\n\n"; 104 | } 105 | msg += `${this.rag_tootip}`; 106 | } 107 | if (_website_message || _inference_message) { 108 | msg += "\n\n"; 109 | msg += _website_message || _inference_message; 110 | } 111 | this.menu.tooltip = msg; 112 | } else if (!userLogin.secret_api_key()) { 113 | this.menu.text = `$(account) Refact.ai`; 114 | this.menu.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); 115 | this.menu.tooltip = _website_message || `Click to login`; 116 | } else { 117 | this.menu.text = this.rag_status || `$(codify-logo) Refact.ai`; 118 | this.menu.backgroundColor = undefined; 119 | let reach = global.rust_binary_blob ? global.rust_binary_blob.attemping_to_reach() : ""; 120 | this.menu.tooltip = _website_message || _inference_message || `Refact Plugin\nCommunicating with server '${reach}'`; 121 | if (this.rag_tootip) { 122 | this.menu.tooltip += `\n\n${this.rag_tootip}`; 123 | } 124 | } 125 | } 126 | 127 | statusbar_spinner(on: boolean) 128 | { 129 | this.spinner = on; 130 | this.choose_color(); 131 | } 132 | 133 | set_socket_error(error: boolean, detail: string|undefined) 134 | { 135 | this.socketerror = error; 136 | if (typeof detail === "string") { 137 | if (detail.length > 100) { 138 | detail = detail.substring(0, 100) + "..."; 139 | } 140 | if (detail !== "{}") { 141 | this.socketerror_msg = `${detail}`; 142 | } else { 143 | this.socketerror_msg = ""; 144 | } 145 | } else { 146 | this.socketerror_msg = ""; 147 | } 148 | if (this.socketerror) { 149 | this.last_model_name = ""; 150 | } 151 | if (error) { 152 | this.have_completion_success = false; 153 | } 154 | this.choose_color(); 155 | } 156 | 157 | set_access_level(state: number) 158 | { 159 | this.access_level = state; 160 | this.choose_color(); 161 | } 162 | 163 | completion_model_worked(model_name: string) 164 | { 165 | this.last_model_name = model_name; 166 | this.have_completion_success = true; 167 | this.choose_color(); 168 | } 169 | 170 | ast_status_limit_reached() { 171 | this.ast_limit_hit = true; 172 | this.choose_color(); 173 | } 174 | 175 | vecdb_status_limit_reached() { 176 | this.vecdb_limit_hit = true; 177 | this.choose_color(); 178 | } 179 | 180 | vecdb_error(error: string) { 181 | this.vecdb_warning = error; 182 | this.choose_color(); 183 | } 184 | 185 | update_rag_status(status: RagStatus) 186 | { 187 | this.rag_status = ''; 188 | if (status.vecdb && !["done", "idle", "cooldown"].includes(status.vecdb.state)) { 189 | const vecdb_parsed_qty = status.vecdb.files_total - status.vecdb.files_unprocessed; 190 | this.rag_status = `$(sync~spin) VecDB ${vecdb_parsed_qty}/${status.vecdb.files_total}`; 191 | } 192 | if (status.ast && !["done", "idle"].includes(status.ast.state)) { 193 | if (status.ast.state === "parsing") { 194 | const ast_parsed_qty = status.ast.files_total - status.ast.files_unparsed; 195 | this.rag_status = `$(sync~spin) Parsing ${ast_parsed_qty}/${status.ast.files_total} `; 196 | } else if (status.ast.state === "indexing") { 197 | this.rag_status = `$(sync~spin) Indexing AST`; 198 | } else if (status.ast.state === "starting") { 199 | this.rag_status = `$(sync~spin) Starting`; 200 | } 201 | } 202 | 203 | let rag_tootip = ''; 204 | if (status.ast) { 205 | rag_tootip += 206 | `AST files: ${status.ast.ast_index_files_total}\n` + 207 | `AST symbols: ${status.ast.ast_index_symbols_total}\n\n` 208 | ; 209 | } else { 210 | rag_tootip += "AST turned off\n\n"; 211 | } 212 | if (status.vecdb) { 213 | rag_tootip += 214 | `VecDB Size: ${status.vecdb.db_size}\n` + 215 | `VecDB Cache: ${status.vecdb.db_cache_size}\n` + 216 | `VecDB this session API calls: ${status.vecdb.requests_made_since_start}\n` + 217 | `VecDB this session vectors requested: ${status.vecdb.vectors_made_since_start}\n\n` 218 | ; 219 | } else { 220 | rag_tootip += "VecDB turned off\n\n"; 221 | } 222 | this.rag_tootip = rag_tootip.trim(); 223 | 224 | this.choose_color(); 225 | } 226 | } 227 | 228 | 229 | async function on_change_active_editor(editor: vscode.TextEditor | undefined) 230 | { 231 | global.status_bar.choose_color(); 232 | } 233 | 234 | 235 | export function status_bar_init() 236 | { 237 | let disposable6 = vscode.window.onDidChangeActiveTextEditor(on_change_active_editor); 238 | let current_editor = vscode.window.activeTextEditor; 239 | if (current_editor) { 240 | on_change_active_editor(current_editor); 241 | } 242 | return [disposable6]; 243 | } 244 | 245 | export async function send_network_problems_to_status_bar( 246 | positive: boolean, 247 | scope: string, 248 | related_url: string, 249 | error_message: string | any, 250 | model_name: string | undefined, 251 | ) { 252 | let invalid_session = false; 253 | let timedout = false; 254 | let conn_refused = false; 255 | if (typeof error_message !== "string") { 256 | if (error_message.code && error_message.code.includes("INVALID_SESSION")) { 257 | invalid_session = true; 258 | } 259 | if (error_message.code && error_message.code.includes("ETIMEDOUT")) { 260 | timedout = true; 261 | } 262 | if (error_message.code && error_message.code.includes("ECONNREFUSED")) { 263 | conn_refused = true; 264 | } 265 | if (error_message instanceof Error && error_message.message) { 266 | error_message = error_message.message; 267 | } else { 268 | error_message = JSON.stringify(error_message); 269 | } 270 | } 271 | if (typeof error_message === "string") { 272 | if (error_message.includes("INVALID_SESSION")) { 273 | invalid_session = true; 274 | } 275 | if (error_message.includes("ETIMEDOUT") || error_message.includes("timed out")) { 276 | timedout = true; 277 | } 278 | if (error_message.includes("ECONNREFUSED")) { 279 | conn_refused = true; 280 | } 281 | } 282 | if (!positive) { 283 | global.side_panel?.chat?.handleStreamEnd(); 284 | await fetchH2.disconnectAll(); 285 | } else { 286 | global.last_positive_result = Date.now(); 287 | } 288 | if (error_message.length > 200) { 289 | error_message = error_message.substring(0, 200) + "…"; 290 | } 291 | global.status_bar.set_socket_error(!positive, error_message); 292 | } 293 | 294 | export default StatusBarMenu; 295 | -------------------------------------------------------------------------------- /src/storeVersions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | 4 | 5 | export function filename_from_document(document: vscode.TextDocument): string 6 | { 7 | let file_name = document.uri.toString(); 8 | let project_dir = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.toString(); 9 | if (project_dir !== undefined && file_name.startsWith(project_dir)) { 10 | // This prevents unnecessary user name and directory details from leaking 11 | let relative_file_name = file_name.substring(project_dir.length); 12 | if (relative_file_name.startsWith("/")) { 13 | relative_file_name = relative_file_name.substring(1); 14 | } 15 | return relative_file_name; 16 | } 17 | // As a fallback, return the full file name without any directory 18 | let last_slash = file_name.lastIndexOf("/"); 19 | if (last_slash >= 0) { 20 | return file_name.substring(last_slash+1); 21 | } 22 | return file_name; 23 | } -------------------------------------------------------------------------------- /src/usabilityHints.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { toPascalCase } from 'refact-chat-js/dist/events'; 3 | import * as vscode from 'vscode'; 4 | 5 | const HTML_TAG_A_REGULAR_EXPRESSION = /([^<]+)<\/a>/i; 6 | 7 | export async function show_message_from_server(kind_of_message: string, msg: string) 8 | { 9 | // Show a message from the server, but only once. 10 | let context_: vscode.ExtensionContext | undefined = global.global_context; 11 | if (context_ === undefined) { 12 | return false; 13 | } 14 | let context = context_ as vscode.ExtensionContext; 15 | let already_seen = context.globalState.get(`refactai.servermsg${kind_of_message}`); 16 | 17 | if (already_seen === undefined) { 18 | already_seen = ""; 19 | } 20 | if (already_seen === msg) { 21 | return false; 22 | } 23 | if (msg === "") { 24 | return false; 25 | } 26 | 27 | const message_match_link = msg.match(HTML_TAG_A_REGULAR_EXPRESSION); 28 | 29 | let message_text = msg; 30 | let link_label: string | undefined; 31 | let link_href: string | undefined; 32 | 33 | if (message_match_link) { 34 | link_href = message_match_link[1]; 35 | link_label = message_match_link[2]; 36 | message_text = msg.replace(HTML_TAG_A_REGULAR_EXPRESSION, link_label); 37 | } 38 | 39 | if (link_href && link_label) { 40 | const button_label = toPascalCase(link_label); 41 | vscode.window.showInformationMessage( 42 | message_text, 43 | button_label 44 | ).then((selection) => { 45 | if (selection === button_label && link_href) { 46 | try { 47 | const uri = vscode.Uri.parse(link_href, true); 48 | vscode.env.openExternal(uri) 49 | .then( 50 | success => { 51 | if (!success) { 52 | vscode.window.showErrorMessage("Failed to open URL"); 53 | } 54 | }, 55 | error => { 56 | vscode.window.showErrorMessage(`Failed to open URL: ${error}`); 57 | } 58 | ); 59 | } catch (error) { 60 | console.error(error); 61 | vscode.window.showErrorMessage(`Failed to open URL: ${error}`); 62 | } 63 | } 64 | }); 65 | } else { 66 | vscode.window.showInformationMessage(msg); 67 | } 68 | 69 | await context.globalState.update(`refactai.servermsg${kind_of_message}`, msg); 70 | } 71 | -------------------------------------------------------------------------------- /src/userLogin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import * as vscode from 'vscode'; 3 | import * as fetchH2 from 'fetch-h2'; 4 | import * as usabilityHints from "./usabilityHints"; 5 | import * as statusBar from "./statusBar"; 6 | 7 | 8 | export function get_address(): string 9 | { 10 | let addr1: string|undefined = vscode.workspace.getConfiguration().get("refactai.addressURL"); 11 | let addr2: string|undefined = vscode.workspace.getConfiguration().get("refactai.infurl"); // old name 12 | return addr1 || addr2 || ""; 13 | } 14 | 15 | 16 | export async function login_message() 17 | { 18 | await vscode.commands.executeCommand('workbench.view.extension.refact-toolbox-pane'); 19 | } 20 | 21 | 22 | export async function welcome_message() 23 | { 24 | await vscode.commands.executeCommand('workbench.view.extension.refact-toolbox-pane'); 25 | await new Promise(resolve => setTimeout(resolve, 1000)); 26 | await vscode.commands.executeCommand('workbench.view.extension.refact-toolbox-pane'); 27 | let selection = await vscode.window.showInformationMessage("Welcome to Refact.ai!\nConnect to AI inference server in sidebar."); 28 | } 29 | 30 | 31 | export async function account_message(info: string, action: string, url: string) 32 | { 33 | let selection = await vscode.window.showInformationMessage( 34 | info, 35 | action, 36 | ); 37 | if (selection === action) { 38 | vscode.env.openExternal(vscode.Uri.parse(url)); 39 | } 40 | } 41 | 42 | 43 | export function secret_api_key(): string 44 | { 45 | let key = vscode.workspace.getConfiguration().get('refactai.apiKey'); 46 | if (!key) { 47 | // Backward compatibility: codify is the old name 48 | key = vscode.workspace.getConfiguration().get('codify.apiKey'); 49 | } 50 | if (!key) { return ""; } 51 | if (typeof key !== 'string') { return ""; } 52 | return key; 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020", 8 | "ES2021.WeakRef", 9 | "DOM", 10 | ], 11 | "sourceMap": true, 12 | "rootDir": "src", 13 | "strict": true, 14 | "esModuleInterop": true, 15 | /* enable all strict type-checking options */ 16 | /* Additional Checks */ 17 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 18 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 19 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 20 | } 21 | } 22 | --------------------------------------------------------------------------------