├── .editorconfig ├── .gitignore ├── .npmrc ├── .reclinerules ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CLINE.LICENSE.md ├── LICENSE.md ├── README.md ├── assets └── icons │ ├── recline.png │ ├── recline.svg │ ├── recline_dark.svg │ └── recline_light.svg ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── repomix.config.json ├── rsbuild.config.ts ├── src ├── extension │ ├── api │ │ ├── index.ts │ │ ├── providers │ │ │ ├── anthropic.ts │ │ │ ├── bedrock.ts │ │ │ ├── deepseek.ts │ │ │ ├── gemini.ts │ │ │ ├── lmstudio.ts │ │ │ ├── ollama.ts │ │ │ ├── openai-native.ts │ │ │ ├── openai.ts │ │ │ ├── openrouter.ts │ │ │ ├── vertex.ts │ │ │ └── vscode-lm.ts │ │ └── transform │ │ │ ├── gemini-format.ts │ │ │ ├── o1-format.ts │ │ │ ├── openai-format.ts │ │ │ ├── stream.ts │ │ │ └── vscode-lm-format.ts │ ├── constants.ts │ ├── core │ │ ├── Recline.ts │ │ ├── assistant-message │ │ │ ├── diff.ts │ │ │ ├── index.ts │ │ │ └── parse-assistant-message.ts │ │ ├── mentions │ │ │ ├── MentionContentFetcher.ts │ │ │ ├── MentionParser.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── prompts │ │ │ ├── responses.ts │ │ │ ├── system.ts │ │ │ └── user-system.ts │ │ ├── sliding-window │ │ │ └── index.ts │ │ └── webview │ │ │ ├── ReclineProvider.ts │ │ │ ├── getNonce.ts │ │ │ └── getUri.ts │ ├── exports │ │ ├── README.md │ │ ├── index.ts │ │ └── recline.d.ts │ ├── index.ts │ ├── integrations │ │ ├── browser │ │ │ └── BrowserSession.ts │ │ ├── diagnostics │ │ │ └── index.ts │ │ ├── editor │ │ │ ├── DecorationController.ts │ │ │ ├── DiffViewProvider.ts │ │ │ └── detect-omission.ts │ │ ├── misc │ │ │ ├── export-markdown.ts │ │ │ ├── extract-text.ts │ │ │ ├── open-file.ts │ │ │ └── process-images.ts │ │ ├── notifications │ │ │ ├── README.md │ │ │ └── index.ts │ │ ├── terminal │ │ │ ├── TerminalManager.ts │ │ │ ├── TerminalProcess.ts │ │ │ ├── TerminalRegistry.ts │ │ │ └── types.ts │ │ ├── theme │ │ │ ├── default-themes │ │ │ │ ├── dark_modern.json │ │ │ │ ├── dark_plus.json │ │ │ │ ├── dark_vs.json │ │ │ │ ├── hc_black.json │ │ │ │ ├── hc_light.json │ │ │ │ ├── light_modern.json │ │ │ │ ├── light_plus.json │ │ │ │ └── light_vs.json │ │ │ └── getTheme.ts │ │ └── workspace │ │ │ ├── WorkspaceTracker.ts │ │ │ ├── environment-cache.ts │ │ │ ├── get-env-info.ts │ │ │ ├── get-js-env.ts │ │ │ └── get-python-env.ts │ ├── services │ │ ├── browser │ │ │ └── urlToMarkdown.ts │ │ ├── fd │ │ │ └── index.ts │ │ ├── mcp │ │ │ └── McpHub.ts │ │ ├── ripgrep │ │ │ └── index.ts │ │ └── tree-sitter │ │ │ ├── index.ts │ │ │ ├── languageParser.ts │ │ │ ├── queries │ │ │ ├── bash.ts │ │ │ ├── c-sharp.ts │ │ │ ├── c.ts │ │ │ ├── cpp.ts │ │ │ ├── css.ts │ │ │ ├── elisp.ts │ │ │ ├── elixir.ts │ │ │ ├── elm.ts │ │ │ ├── embedded_template.ts │ │ │ ├── go.ts │ │ │ ├── html.ts │ │ │ ├── index.ts │ │ │ ├── java.ts │ │ │ ├── javascript.ts │ │ │ ├── json.ts │ │ │ ├── kotlin.ts │ │ │ ├── lua.ts │ │ │ ├── objc.ts │ │ │ ├── ocaml.ts │ │ │ ├── php.ts │ │ │ ├── python.ts │ │ │ ├── ql.ts │ │ │ ├── rescript.ts │ │ │ ├── ruby.ts │ │ │ ├── rust.ts │ │ │ ├── scala.ts │ │ │ ├── solidity.ts │ │ │ ├── swift.ts │ │ │ ├── systemrdl.ts │ │ │ ├── tlaplus.ts │ │ │ ├── toml.ts │ │ │ ├── tsx.ts │ │ │ ├── typescript.ts │ │ │ ├── vue.ts │ │ │ ├── yaml.ts │ │ │ └── zig.ts │ │ │ ├── supported.ts │ │ │ ├── types.ts │ │ │ └── wasm.ts │ ├── shims.d.ts │ ├── test │ │ └── extension.test.ts │ └── utils │ │ ├── cost.test.ts │ │ ├── cost.ts │ │ ├── fs.test.ts │ │ ├── fs.ts │ │ ├── path.test.ts │ │ ├── path.ts │ │ ├── sanitize.ts │ │ └── string.ts ├── shared │ ├── AutoApprovalSettings.ts │ ├── ExtensionMessage.ts │ ├── HistoryItem.ts │ ├── WebviewMessage.ts │ ├── api.ts │ ├── array.test.ts │ ├── array.ts │ ├── combineApiRequests.ts │ ├── combineCommandSequences.ts │ ├── context-mentions.ts │ ├── getApiMetrics.ts │ ├── mcp.ts │ └── vsCodeSelectorUtils.ts └── webview-ui │ ├── App.tsx │ ├── components │ ├── chat │ │ ├── Announcement.tsx │ │ ├── AutoApproveMenu.tsx │ │ ├── BrowserSessionRow.tsx │ │ ├── ChatRow.tsx │ │ ├── ChatTextArea.tsx │ │ ├── ChatView.tsx │ │ ├── ContextMenu.tsx │ │ └── TaskHeader.tsx │ ├── common │ │ ├── CodeAccordian.tsx │ │ ├── CodeBlock.tsx │ │ ├── MarkdownBlock.tsx │ │ ├── Thumbnails.tsx │ │ └── VSCodeButtonLink.tsx │ ├── history │ │ ├── HistoryPreview.tsx │ │ └── HistoryView.tsx │ ├── mcp │ │ ├── McpResourceRow.tsx │ │ ├── McpToolRow.tsx │ │ └── McpView.tsx │ ├── settings │ │ ├── ApiOptions.tsx │ │ ├── OpenRouterModelPicker.tsx │ │ ├── SettingsView.tsx │ │ └── TabNavbar.tsx │ └── welcome │ │ └── WelcomeView.tsx │ ├── context │ └── ExtensionStateContext.tsx │ ├── index.css │ ├── index.tsx │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── utils │ ├── context-mentions.ts │ ├── format.ts │ ├── getLanguageFromPath.ts │ ├── mcp.ts │ ├── textMateToHljs.ts │ ├── validate.ts │ └── vscode.ts ├── test └── index.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator=true 2 | ignore-workspace-root-check=true 3 | -------------------------------------------------------------------------------- /.reclinerules: -------------------------------------------------------------------------------- 1 | **Context** 2 | This is the codebase of the Recline VSCode extension itself. 3 | Recline is an autonomous AI assistant that seamlessly integrates with your CLI and editor to create, edit, and run; redefining how you code. 4 | 5 | **Rules** 6 | 1. The project uses unbuild to build (do not change) 7 | 2. The project uses pnpm for dependency management (do not change) 8 | 3. *IGNORE ALL ESLINT ERRORS* 9 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@vscode/test-cli"; 2 | 3 | 4 | export default defineConfig({ 5 | files: "test/**/*.test.ts", 6 | workspaceFolder: ".", 7 | mocha: { 8 | ui: "tdd", 9 | timeout: 20000, // 20 seconds 10 | color: true 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "ms-vscode.extension-test-runner" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}" 10 | ], 11 | "outFiles": [ 12 | "${workspaceFolder}/dist/**/*.js" 13 | ], 14 | "preLaunchTask": "npm: dev" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/test" 23 | ], 24 | "outFiles": [ 25 | "${workspaceFolder}/dist/**/*.js" 26 | ], 27 | "preLaunchTask": "npm: dev" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit", 7 | "source.fixAll.stylelint": "explicit" 8 | }, 9 | 10 | "eslint.rules.customizations": [ 11 | { "rule": "style/*", "severity": "off", "fixable": true }, 12 | { "rule": "format/*", "severity": "off", "fixable": true }, 13 | { "rule": "*-indent", "severity": "off", "fixable": true }, 14 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 15 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 16 | { "rule": "*-order", "severity": "off", "fixable": true }, 17 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 18 | { "rule": "*-newline", "severity": "off", "fixable": true }, 19 | { "rule": "*quotes", "severity": "off", "fixable": true }, 20 | { "rule": "*semi", "severity": "off", "fixable": true } 21 | ], 22 | 23 | "eslint.validate": [ 24 | "javascript", 25 | "javascriptreact", 26 | "typescript", 27 | "typescriptreact", 28 | "html", 29 | "markdown", 30 | "json", 31 | "jsonc", 32 | "yaml", 33 | "css" 34 | ], 35 | "svg.preview.background": "editor" 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "dev", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "isBackground": true, 12 | "presentation": { 13 | "reveal": "never", 14 | "panel": "dedicated" 15 | }, 16 | "problemMatcher": { 17 | "owner": "typescript", 18 | "fileLocation": "relative", 19 | "pattern": { 20 | "regexp": "^([^\\s].*)\\((\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", 21 | "file": 1, 22 | "location": 2, 23 | "severity": 3, 24 | "code": 4, 25 | "message": 5 26 | }, 27 | "background": { 28 | "activeOnStart": true, 29 | "beginsPattern": "^start Building", 30 | "endsPattern": "^ready Built in" 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /assets/icons/recline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julesmons/recline-legacy/ddf784b56c1ad6277cf229e13bd2070a17eba315/assets/icons/recline.png -------------------------------------------------------------------------------- /assets/icons/recline.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/icons/recline_dark.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/icons/recline_light.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from "@antfu/eslint-config"; 3 | 4 | 5 | export default antfu( 6 | { 7 | type: "lib", 8 | stylistic: { 9 | indent: 2, 10 | quotes: "double", 11 | semi: true, 12 | overrides: { 13 | "style/comma-dangle": ["error", "never"], 14 | "style/padded-blocks": ["off"], 15 | "style/indent": ["error", 2, { 16 | offsetTernaryExpressions: false, 17 | SwitchCase: 1 18 | }], 19 | "style/brace-style": ["error", "stroustrup", { allowSingleLine: false }], 20 | "style/no-multiple-empty-lines": ["error", { maxBOF: 0, max: 2, maxEOF: 1 }], 21 | "import/newline-after-import": ["error", { 22 | count: 2, 23 | exactCount: true, 24 | considerComments: true 25 | }], 26 | "grouped-accessor-pairs": ["error", "getBeforeSet"], 27 | "perfectionist/sort-imports": [ 28 | "error", 29 | { 30 | type: "line-length", 31 | order: "asc", 32 | groups: [ 33 | // Types 34 | "builtin-type", 35 | "external-type", 36 | "internal-type", 37 | "aliased-shared-type", 38 | "aliased-extension-type", 39 | "aliased-webview-ui-type", 40 | "parent-type", 41 | "sibling-type", 42 | "index-type", 43 | "type", 44 | // Modules 45 | "builtin", 46 | "external", 47 | "internal", 48 | "aliased-shared", 49 | "aliased-extension", 50 | "aliased-webview-ui", 51 | "parent", 52 | "sibling", 53 | "index", 54 | "side-effect", 55 | // Default 56 | "object", 57 | "unknown" 58 | ], 59 | newlinesBetween: "always", 60 | customGroups: { 61 | value: { 62 | "aliased-shared": [/^@shared\/.*/], 63 | "aliased-extension": [/^@extension\/.*/], 64 | "aliased-webview-ui": [/^@webview-ui\/.*/] 65 | }, 66 | type: { 67 | "aliased-shared-type": [/^@shared\/.*/], 68 | "aliased-extension-type": [/^@extension\/.*/], 69 | "aliased-webview-ui-type": [/^@webview-ui\/.*/] 70 | } 71 | } 72 | } 73 | ], 74 | "perfectionist/sort-classes": [ 75 | "error", 76 | { 77 | type: "natural", 78 | groups: [ 79 | "index-signature", 80 | "static-property", 81 | "static-block", 82 | "decorated-property", 83 | "protected-property", 84 | "private-property", 85 | "property", 86 | "decorated-accessor-property", 87 | "protected-accessor-property", 88 | "private-accessor-property", 89 | "accessor-property", 90 | ["get-method", "set-method"], 91 | "constructor", 92 | "decorated-method", 93 | "static-method", 94 | "protected-method", 95 | "private-method", 96 | "method", 97 | "unknown" 98 | ] 99 | } 100 | ] 101 | } 102 | }, 103 | typescript: { 104 | tsconfigPath: "./tsconfig.json", 105 | overrides: { 106 | "ts/prefer-enum-initializers": ["error"], 107 | "ts/explicit-function-return-type": ["error", { 108 | allowDirectConstAssertionInArrowFunctions: false, 109 | allowIIFEs: true 110 | }], 111 | "ts/consistent-type-imports": ["error", { prefer: "type-imports" }] 112 | } 113 | }, 114 | jsonc: { 115 | overrides: { 116 | "jsonc/comma-dangle": ["error", "never"] 117 | } 118 | }, 119 | formatters: { 120 | css: true 121 | } 122 | } 123 | ); 124 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | -------------------------------------------------------------------------------- /repomix.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": { 3 | "filePath": "repomix-output.xml", 4 | "style": "xml", 5 | "removeComments": false, 6 | "removeEmptyLines": false, 7 | "topFilesLength": 5, 8 | "showLineNumbers": false, 9 | "copyToClipboard": false 10 | }, 11 | "include": [ 12 | "./src/**/*.ts", 13 | "./src/**/*.tsx", 14 | "./src/**/*.css" 15 | ], 16 | "ignore": { 17 | "useGitignore": true, 18 | "useDefaultPatterns": true, 19 | "customPatterns": [] 20 | }, 21 | "security": { 22 | "enableSecurityCheck": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@rsbuild/core"; 2 | import { pluginPreact } from "@rsbuild/plugin-preact"; 3 | 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: { 8 | "react": "./node_modules/preact/compat", 9 | "react-dom": "./node_modules/preact/compat", 10 | "@shared": "./src/shared", 11 | "@extension": "./src/extension", 12 | "@webview-ui": "./src/webview-ui" 13 | } 14 | }, 15 | output: { 16 | externals: ["vscode"] 17 | }, 18 | performance: { 19 | chunkSplit: { 20 | strategy: "all-in-one" 21 | } 22 | }, 23 | environments: { 24 | extension: { 25 | source: { 26 | entry: { 27 | index: "./src/extension/index.ts" 28 | } 29 | }, 30 | output: { 31 | target: "node", 32 | filename: { 33 | js: "extension.js" 34 | }, 35 | distPath: { 36 | root: "dist", 37 | js: "", 38 | jsAsync: "", 39 | css: "css", 40 | cssAsync: "css", 41 | svg: "imgs", 42 | font: "fonts", 43 | html: "", 44 | wasm: "wasm", 45 | image: "imgs", 46 | media: "assets", 47 | assets: "assets" 48 | } 49 | } 50 | }, 51 | webview: { 52 | source: { 53 | entry: { 54 | index: "./src/webview-ui/index.tsx" 55 | } 56 | }, 57 | output: { 58 | target: "web-worker", 59 | emitCss: true, 60 | assetPrefix: ".", 61 | filename: { 62 | js: "webview.js", 63 | css: "webview.css" 64 | }, 65 | distPath: { 66 | root: "dist", 67 | js: "", 68 | jsAsync: "", 69 | css: "", 70 | cssAsync: "", 71 | svg: "", 72 | font: "", 73 | html: "", 74 | wasm: "", 75 | image: "", 76 | media: "", 77 | assets: "assets" 78 | } 79 | }, 80 | plugins: [pluginPreact()] 81 | } 82 | }, 83 | tools: { 84 | rspack: { 85 | ignoreWarnings: [ 86 | /Critical dependency/ 87 | ], 88 | output: { 89 | asyncChunks: false 90 | } 91 | }, 92 | bundlerChain: (chain) => { 93 | chain.module 94 | .rule("RULE.WASM") 95 | .test(/tree-sitter(?:-.+)?\.wasm$/) 96 | .type("asset/resource"); 97 | } 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /src/extension/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiConfiguration, MessageParamWithTokenCount, ModelInfo } from "@shared/api"; 2 | 3 | import type { ApiStream } from "./transform/stream"; 4 | 5 | import { GeminiModelProvider } from "./providers/gemini"; 6 | import { OllamaModelProvider } from "./providers/ollama"; 7 | import { OpenAIModelProvider } from "./providers/openai"; 8 | import { VertexModelProvider } from "./providers/vertex"; 9 | import { BedrockModelProvider } from "./providers/bedrock"; 10 | import { DeepSeekModelProvider } from "./providers/deepseek"; 11 | import { LmStudioModelProvider } from "./providers/lmstudio"; 12 | import { VSCodeLmModelProvider } from "./providers/vscode-lm"; 13 | import { AnthropicModelProvider } from "./providers/anthropic"; 14 | import { OpenRouterModelProvider } from "./providers/openrouter"; 15 | import { OpenAiNativeModelProvider } from "./providers/openai-native"; 16 | 17 | 18 | export interface Model { 19 | id: string; 20 | info: ModelInfo; 21 | } 22 | export interface ModelProvider { 23 | createMessage: (systemPrompt: string, messages: MessageParamWithTokenCount[]) => ApiStream; 24 | getModel: () => Promise; 25 | dispose: () => Promise; 26 | } 27 | 28 | export function buildApiHandler(configuration: ApiConfiguration): ModelProvider { 29 | const { apiProvider, ...options } = configuration; 30 | switch (apiProvider) { 31 | case "anthropic": 32 | return new AnthropicModelProvider(options); 33 | case "openrouter": 34 | return new OpenRouterModelProvider(options); 35 | case "bedrock": 36 | return new BedrockModelProvider(options); 37 | case "vertex": 38 | return new VertexModelProvider(options); 39 | case "openai": 40 | return new OpenAIModelProvider(options); 41 | case "ollama": 42 | return new OllamaModelProvider(options); 43 | case "lmstudio": 44 | return new LmStudioModelProvider(options); 45 | case "gemini": 46 | return new GeminiModelProvider(options); 47 | case "openai-native": 48 | return new OpenAiNativeModelProvider(options); 49 | case "deepseek": 50 | return new DeepSeekModelProvider(options); 51 | case "vscode-lm": 52 | return new VSCodeLmModelProvider(options); 53 | case undefined: 54 | default: 55 | throw new Error("The selected API provider is not supported"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/extension/api/providers/bedrock.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { ApiHandlerOptions, BedrockModelId, ModelInfo } from "@shared/api"; 4 | 5 | import type { ModelProvider } from "../"; 6 | import type { ApiStream } from "../transform/stream"; 7 | 8 | import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"; 9 | 10 | import { bedrockDefaultModelId, bedrockModels } from "@shared/api"; 11 | 12 | 13 | // https://docs.anthropic.com/en/api/claude-on-amazon-bedrock 14 | export class BedrockModelProvider implements ModelProvider { 15 | private client: AnthropicBedrock; 16 | private options: ApiHandlerOptions; 17 | 18 | constructor(options: ApiHandlerOptions) { 19 | this.options = options; 20 | this.client = new AnthropicBedrock({ 21 | // Authenticate by either providing the keys below or use the default AWS credential providers, such as 22 | // using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables. 23 | ...(this.options.awsAccessKey ? { awsAccessKey: this.options.awsAccessKey } : {}), 24 | ...(this.options.awsSecretKey ? { awsSecretKey: this.options.awsSecretKey } : {}), 25 | ...(this.options.awsSessionToken ? { awsSessionToken: this.options.awsSessionToken } : {}), 26 | 27 | // awsRegion changes the aws region to which the request is made. By default, we read AWS_REGION, 28 | // and if that's not present, we default to us-east-1. Note that we do not read ~/.aws/config for the region. 29 | awsRegion: this.options.awsRegion 30 | }); 31 | } 32 | 33 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 34 | // cross region inference requires prefixing the model id with the region 35 | const model = await this.getModel(); 36 | let modelId: string; 37 | if (this.options.awsUseCrossRegionInference) { 38 | const regionPrefix = (this.options.awsRegion || "").slice(0, 3); 39 | switch (regionPrefix) { 40 | case "us-": 41 | modelId = `us.${model.id}`; 42 | break; 43 | case "eu-": 44 | modelId = `eu.${model.id}`; 45 | break; 46 | default: 47 | // cross region inference is not supported in this region, falling back to default model 48 | modelId = model.id; 49 | break; 50 | } 51 | } 52 | else { 53 | modelId = model.id; 54 | } 55 | 56 | const stream = await this.client.messages.create({ 57 | model: modelId, 58 | max_tokens: model.info.maxTokens || 8192, 59 | temperature: 0, 60 | system: systemPrompt, 61 | messages, 62 | stream: true 63 | }); 64 | for await (const chunk of stream) { 65 | switch (chunk.type) { 66 | case "message_start": 67 | const usage = chunk.message.usage; 68 | yield { 69 | type: "usage", 70 | inputTokens: usage.input_tokens || 0, 71 | outputTokens: usage.output_tokens || 0 72 | }; 73 | break; 74 | case "message_delta": 75 | yield { 76 | type: "usage", 77 | inputTokens: 0, 78 | outputTokens: chunk.usage.output_tokens || 0 79 | }; 80 | break; 81 | 82 | case "content_block_start": 83 | switch (chunk.content_block.type) { 84 | case "text": 85 | if (chunk.index > 0) { 86 | yield { 87 | type: "text", 88 | text: "\n" 89 | }; 90 | } 91 | yield { 92 | type: "text", 93 | text: chunk.content_block.text 94 | }; 95 | break; 96 | } 97 | break; 98 | case "content_block_delta": 99 | switch (chunk.delta.type) { 100 | case "text_delta": 101 | yield { 102 | type: "text", 103 | text: chunk.delta.text 104 | }; 105 | break; 106 | } 107 | break; 108 | } 109 | } 110 | } 111 | 112 | async getModel(): Promise<{ id: string; info: ModelInfo }> { 113 | const modelId = this.options.apiModelId; 114 | if (modelId && modelId in bedrockModels) { 115 | const id = modelId as BedrockModelId; 116 | return { id, info: bedrockModels[id] }; 117 | } 118 | return { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/extension/api/providers/deepseek.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { ApiHandlerOptions, DeepSeekModelId, ModelInfo } from "@shared/api"; 4 | 5 | import type { ModelProvider } from "../"; 6 | import type { ApiStream } from "../transform/stream"; 7 | 8 | import OpenAI from "openai"; 9 | 10 | import { deepSeekDefaultModelId, deepSeekModels } from "@shared/api"; 11 | 12 | import { convertToOpenAiMessages } from "../transform/openai-format"; 13 | 14 | 15 | export class DeepSeekModelProvider implements ModelProvider { 16 | private client: OpenAI; 17 | private options: ApiHandlerOptions; 18 | 19 | constructor(options: ApiHandlerOptions) { 20 | this.options = options; 21 | this.client = new OpenAI({ 22 | baseURL: "https://api.deepseek.com/v1", 23 | apiKey: this.options.deepSeekApiKey 24 | // Add any additional configuration required by the DeepSeek API 25 | }); 26 | } 27 | 28 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 29 | const model = await this.getModel(); 30 | try { 31 | const stream = await this.client.chat.completions.create({ 32 | model: model.id, 33 | max_tokens: model.info.maxTokens, // Use max_tokens instead of max_completion_tokens 34 | temperature: 0, 35 | messages: [ 36 | { role: "system", content: systemPrompt }, 37 | ...convertToOpenAiMessages(messages) 38 | ], 39 | stream: true, 40 | streamOptions: { includeUsage: true } // Correct the property name if needed 41 | }); 42 | 43 | for await (const chunk of stream) { 44 | if (chunk.choices && chunk.choices.length > 0) { 45 | const delta = chunk.choices[0].delta; 46 | if (delta && delta.content) { 47 | yield { type: "text", text: delta.content }; 48 | } 49 | } 50 | 51 | if (chunk.usage) { 52 | yield { 53 | type: "usage", 54 | inputTokens: chunk.usage.prompt_tokens || 0, 55 | outputTokens: chunk.usage.completion_tokens || 0, 56 | cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0, 57 | cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0 58 | }; 59 | } 60 | } 61 | } 62 | catch (error) { 63 | // Handle any errors that occur during the API call or streaming 64 | console.error("Error in createMessage:", error); 65 | throw error; 66 | } 67 | } 68 | 69 | async dispose(): Promise { 70 | // Nothing to dispose... 71 | } 72 | 73 | async getModel(): Promise<{ id: DeepSeekModelId; info: ModelInfo }> { 74 | const modelId = this.options.apiModelId; 75 | if (modelId && deepSeekModels.hasOwnProperty(modelId)) { 76 | return { id: modelId as DeepSeekModelId, info: deepSeekModels[modelId] }; 77 | } 78 | return { 79 | id: deepSeekDefaultModelId, 80 | info: deepSeekModels[deepSeekDefaultModelId] 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/extension/api/providers/gemini.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { ApiHandlerOptions, GeminiModelId, ModelInfo } from "@shared/api"; 4 | 5 | import type { ModelProvider } from "../"; 6 | import type { ApiStream } from "../transform/stream"; 7 | 8 | import { GoogleGenerativeAI } from "@google/generative-ai"; 9 | 10 | import { geminiDefaultModelId, geminiModels } from "@shared/api"; 11 | 12 | import { convertAnthropicMessageToGemini } from "../transform/gemini-format"; 13 | 14 | 15 | export class GeminiModelProvider implements ModelProvider { 16 | private client: GoogleGenerativeAI; 17 | private options: ApiHandlerOptions; 18 | 19 | constructor(options: ApiHandlerOptions) { 20 | if (!options.geminiApiKey) { 21 | throw new Error("API key is required for Google Gemini"); 22 | } 23 | this.options = options; 24 | this.client = new GoogleGenerativeAI(options.geminiApiKey); 25 | } 26 | 27 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 28 | const info = await this.getModel(); 29 | const model = this.client.getGenerativeModel({ 30 | model: info.id, 31 | systemInstruction: systemPrompt 32 | }); 33 | const result = await model.generateContentStream({ 34 | contents: messages.map(convertAnthropicMessageToGemini), 35 | generationConfig: { 36 | // maxOutputTokens: info.maxTokens, 37 | temperature: 0 38 | } 39 | }); 40 | 41 | for await (const chunk of result.stream) { 42 | yield { 43 | type: "text", 44 | text: chunk.text() 45 | }; 46 | } 47 | 48 | const response = await result.response; 49 | yield { 50 | type: "usage", 51 | inputTokens: response.usageMetadata?.promptTokenCount ?? 0, 52 | outputTokens: response.usageMetadata?.candidatesTokenCount ?? 0 53 | }; 54 | } 55 | 56 | async getModel(): Promise<{ id: GeminiModelId; info: ModelInfo }> { 57 | const modelId = this.options.apiModelId; 58 | if (modelId && modelId in geminiModels) { 59 | const id = modelId as GeminiModelId; 60 | return { id, info: geminiModels[id] }; 61 | } 62 | return { id: geminiDefaultModelId, info: geminiModels[geminiDefaultModelId] }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/extension/api/providers/lmstudio.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { ApiHandlerOptions, ModelInfo } from "@shared/api"; 4 | 5 | import type { ModelProvider } from "../"; 6 | import type { ApiStream } from "../transform/stream"; 7 | 8 | import OpenAI from "openai"; 9 | 10 | import { openAiModelInfoSaneDefaults } from "@shared/api"; 11 | 12 | import { convertToOpenAiMessages } from "../transform/openai-format"; 13 | 14 | 15 | export class LmStudioModelProvider implements ModelProvider { 16 | private client: OpenAI; 17 | private options: ApiHandlerOptions; 18 | 19 | constructor(options: ApiHandlerOptions) { 20 | this.options = options; 21 | this.client = new OpenAI({ 22 | baseURL: `${this.options.lmStudioBaseUrl || "http://localhost:1234"}/v1`, 23 | apiKey: "noop" 24 | }); 25 | } 26 | 27 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 28 | const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ 29 | { role: "system", content: systemPrompt }, 30 | ...convertToOpenAiMessages(messages) 31 | ]; 32 | 33 | try { 34 | const model = await this.getModel(); 35 | const stream = await this.client.chat.completions.create({ 36 | model: model.id, 37 | messages: openAiMessages, 38 | temperature: 0, 39 | stream: true 40 | }); 41 | for await (const chunk of stream) { 42 | const delta = chunk.choices[0]?.delta; 43 | if (delta?.content) { 44 | yield { 45 | type: "text", 46 | text: delta.content 47 | }; 48 | } 49 | } 50 | } 51 | catch (error) { 52 | // LM Studio doesn't return an error code/body for now 53 | throw new Error( 54 | "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Recline's prompts." 55 | ); 56 | } 57 | } 58 | 59 | async getModel(): Promise<{ id: string; info: ModelInfo }> { 60 | return { 61 | id: this.options.lmStudioModelId || "", 62 | info: openAiModelInfoSaneDefaults 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/extension/api/providers/ollama.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { ApiHandlerOptions, ModelInfo } from "@shared/api"; 4 | 5 | import type { ModelProvider } from "../"; 6 | import type { ApiStream } from "../transform/stream"; 7 | 8 | import OpenAI from "openai"; 9 | 10 | import { openAiModelInfoSaneDefaults } from "@shared/api"; 11 | 12 | import { convertToOpenAiMessages } from "../transform/openai-format"; 13 | 14 | 15 | export class OllamaModelProvider implements ModelProvider { 16 | private client: OpenAI; 17 | private options: ApiHandlerOptions; 18 | 19 | constructor(options: ApiHandlerOptions) { 20 | this.options = options; 21 | this.client = new OpenAI({ 22 | baseURL: `${this.options.ollamaBaseUrl || "http://localhost:11434"}/v1`, 23 | apiKey: "ollama" 24 | }); 25 | } 26 | 27 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 28 | const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ 29 | { role: "system", content: systemPrompt }, 30 | ...convertToOpenAiMessages(messages) 31 | ]; 32 | 33 | const model = await this.getModel(); 34 | const stream = await this.client.chat.completions.create({ 35 | model: model.id, 36 | messages: openAiMessages, 37 | temperature: 0, 38 | stream: true 39 | }); 40 | for await (const chunk of stream) { 41 | const delta = chunk.choices[0]?.delta; 42 | if (delta?.content) { 43 | yield { 44 | type: "text", 45 | text: delta.content 46 | }; 47 | } 48 | } 49 | } 50 | 51 | async getModel(): Promise<{ id: string; info: ModelInfo }> { 52 | return { 53 | id: this.options.ollamaModelId || "", 54 | info: openAiModelInfoSaneDefaults 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/extension/api/providers/openai-native.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { 4 | ApiHandlerOptions, 5 | ModelInfo, 6 | OpenAiNativeModelId 7 | } from "@shared/api"; 8 | 9 | import type { ModelProvider } from "../"; 10 | import type { ApiStream } from "../transform/stream"; 11 | 12 | import OpenAI from "openai"; 13 | 14 | import { 15 | openAiNativeDefaultModelId, 16 | openAiNativeModels 17 | } from "@shared/api"; 18 | 19 | import { convertToOpenAiMessages } from "../transform/openai-format"; 20 | 21 | 22 | export class OpenAiNativeModelProvider implements ModelProvider { 23 | private client: OpenAI; 24 | private options: ApiHandlerOptions; 25 | 26 | constructor(options: ApiHandlerOptions) { 27 | this.options = options; 28 | this.client = new OpenAI({ 29 | apiKey: this.options.openAiNativeApiKey 30 | }); 31 | } 32 | 33 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 34 | const model = await this.getModel(); 35 | switch (model.id) { 36 | case "o1-preview": 37 | case "o1-mini": { 38 | // o1 doesnt support streaming, non-1 temp, or system prompt 39 | const response = await this.client.chat.completions.create({ 40 | model: model.id, 41 | messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)] 42 | }); 43 | yield { 44 | type: "text", 45 | text: response.choices[0]?.message.content || "" 46 | }; 47 | yield { 48 | type: "usage", 49 | inputTokens: response.usage?.prompt_tokens || 0, 50 | outputTokens: response.usage?.completion_tokens || 0 51 | }; 52 | break; 53 | } 54 | default: { 55 | const stream = await this.client.chat.completions.create({ 56 | model: model.id, 57 | // max_completion_tokens: model.info.maxTokens, 58 | temperature: 0, 59 | messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], 60 | stream: true, 61 | stream_options: { include_usage: true } 62 | }); 63 | 64 | for await (const chunk of stream) { 65 | const delta = chunk.choices[0]?.delta; 66 | if (delta?.content) { 67 | yield { 68 | type: "text", 69 | text: delta.content 70 | }; 71 | } 72 | 73 | // contains a null value except for the last chunk which contains the token usage statistics for the entire request 74 | if (chunk.usage) { 75 | yield { 76 | type: "usage", 77 | inputTokens: chunk.usage.prompt_tokens || 0, 78 | outputTokens: chunk.usage.completion_tokens || 0 79 | }; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | async getModel(): Promise<{ id: OpenAiNativeModelId; info: ModelInfo }> { 87 | const modelId = this.options.apiModelId; 88 | if (modelId && modelId in openAiNativeModels) { 89 | const id = modelId as OpenAiNativeModelId; 90 | return { id, info: openAiNativeModels[id] }; 91 | } 92 | return { id: openAiNativeDefaultModelId, info: openAiNativeModels[openAiNativeDefaultModelId] }; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/extension/api/providers/openai.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { 4 | ApiHandlerOptions, 5 | ModelInfo 6 | } from "@shared/api"; 7 | 8 | import type { ModelProvider } from "../index"; 9 | import type { ApiStream } from "../transform/stream"; 10 | 11 | import OpenAI, { AzureOpenAI } from "openai"; 12 | 13 | import { 14 | azureOpenAiDefaultApiVersion, 15 | openAiModelInfoSaneDefaults 16 | } from "@shared/api"; 17 | 18 | import { convertToOpenAiMessages } from "../transform/openai-format"; 19 | 20 | 21 | export class OpenAIModelProvider implements ModelProvider { 22 | private client: OpenAI; 23 | private options: ApiHandlerOptions; 24 | 25 | constructor(options: ApiHandlerOptions) { 26 | this.options = options; 27 | // Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai 28 | if (this.options.openAiBaseUrl?.toLowerCase().includes("azure.com")) { 29 | this.client = new AzureOpenAI({ 30 | baseURL: this.options.openAiBaseUrl, 31 | apiKey: this.options.openAiApiKey, 32 | apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion 33 | }); 34 | } 35 | else { 36 | this.client = new OpenAI({ 37 | baseURL: this.options.openAiBaseUrl, 38 | apiKey: this.options.openAiApiKey 39 | }); 40 | } 41 | } 42 | 43 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 44 | const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ 45 | { role: "system", content: systemPrompt }, 46 | ...convertToOpenAiMessages(messages) 47 | ]; 48 | const stream = await this.client.chat.completions.create({ 49 | model: this.options.openAiModelId ?? "", 50 | messages: openAiMessages, 51 | temperature: 0, 52 | stream: true, 53 | stream_options: { include_usage: true } 54 | }); 55 | for await (const chunk of stream) { 56 | const delta = chunk.choices[0]?.delta; 57 | if (delta?.content) { 58 | yield { 59 | type: "text", 60 | text: delta.content 61 | }; 62 | } 63 | if (chunk.usage) { 64 | yield { 65 | type: "usage", 66 | inputTokens: chunk.usage.prompt_tokens || 0, 67 | outputTokens: chunk.usage.completion_tokens || 0 68 | }; 69 | } 70 | } 71 | } 72 | 73 | async getModel(): Promise<{ id: string; info: ModelInfo }> { 74 | return { 75 | id: this.options.openAiModelId ?? "", 76 | info: openAiModelInfoSaneDefaults 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/extension/api/providers/vertex.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import type { ApiHandlerOptions, ModelInfo, VertexModelId } from "@shared/api"; 4 | 5 | import type { ModelProvider } from "../"; 6 | import type { ApiStream } from "../transform/stream"; 7 | 8 | import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; 9 | 10 | import { vertexDefaultModelId, vertexModels } from "@shared/api"; 11 | 12 | 13 | // https://docs.anthropic.com/en/api/claude-on-vertex-ai 14 | export class VertexModelProvider implements ModelProvider { 15 | private client: AnthropicVertex; 16 | private options: ApiHandlerOptions; 17 | 18 | constructor(options: ApiHandlerOptions) { 19 | this.options = options; 20 | this.client = new AnthropicVertex({ 21 | projectId: this.options.vertexProjectId, 22 | // https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions 23 | region: this.options.vertexRegion 24 | }); 25 | } 26 | 27 | async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { 28 | const model = await this.getModel(); 29 | const stream = await this.client.messages.create({ 30 | model: model.id, 31 | max_tokens: model.info.maxTokens || 8192, 32 | temperature: 0, 33 | system: systemPrompt, 34 | messages, 35 | stream: true 36 | }); 37 | for await (const chunk of stream) { 38 | switch (chunk.type) { 39 | case "message_start": 40 | const usage = chunk.message.usage; 41 | yield { 42 | type: "usage", 43 | inputTokens: usage.input_tokens || 0, 44 | outputTokens: usage.output_tokens || 0 45 | }; 46 | break; 47 | case "message_delta": 48 | yield { 49 | type: "usage", 50 | inputTokens: 0, 51 | outputTokens: chunk.usage.output_tokens || 0 52 | }; 53 | break; 54 | 55 | case "content_block_start": 56 | switch (chunk.content_block.type) { 57 | case "text": 58 | if (chunk.index > 0) { 59 | yield { 60 | type: "text", 61 | text: "\n" 62 | }; 63 | } 64 | yield { 65 | type: "text", 66 | text: chunk.content_block.text 67 | }; 68 | break; 69 | } 70 | break; 71 | case "content_block_delta": 72 | switch (chunk.delta.type) { 73 | case "text_delta": 74 | yield { 75 | type: "text", 76 | text: chunk.delta.text 77 | }; 78 | break; 79 | } 80 | break; 81 | } 82 | } 83 | } 84 | 85 | async getModel(): Promise<{ id: VertexModelId; info: ModelInfo }> { 86 | const modelId = this.options.apiModelId; 87 | if (modelId && modelId in vertexModels) { 88 | const id = modelId as VertexModelId; 89 | return { id, info: vertexModels[id] }; 90 | } 91 | return { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/extension/api/transform/stream.ts: -------------------------------------------------------------------------------- 1 | export type ApiStream = AsyncGenerator; 2 | export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk; 3 | 4 | export interface ApiStreamTextChunk { 5 | type: "text"; 6 | text: string; 7 | } 8 | 9 | export interface ApiStreamUsageChunk { 10 | type: "usage"; 11 | inputTokens: number; 12 | outputTokens: number; 13 | cacheWriteTokens?: number; 14 | cacheReadTokens?: number; 15 | totalCost?: number; // openrouter 16 | } 17 | -------------------------------------------------------------------------------- /src/extension/constants.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | 4 | export const GlobalFileNames = { 5 | apiConversationHistory: "api_conversation_history.json", 6 | uiMessages: "ui_messages.json", 7 | openRouterModels: "openrouter_models.json", 8 | mcpSettings: "recline_mcp_settings.json", 9 | reclineRules: ".reclinerules" 10 | }; 11 | 12 | const extension = vscode.extensions.getExtension("julesmons.recline")!; 13 | export const workspaceRoot: string = vscode.workspace.workspaceFolders?.map(folder => folder.uri.fsPath).at(0) ?? "."; 14 | 15 | export const extensionPath: string = extension.extensionPath; 16 | export const extensionUri: vscode.Uri = extension.extensionUri; 17 | 18 | export const reclineRulesPath: string = `${workspaceRoot}/${GlobalFileNames.reclineRules}`; 19 | -------------------------------------------------------------------------------- /src/extension/core/assistant-message/index.ts: -------------------------------------------------------------------------------- 1 | export type AssistantMessageContent = TextContent | ToolUse; 2 | 3 | export { parseAssistantMessage } from "./parse-assistant-message"; 4 | 5 | export interface TextContent { 6 | type: "text"; 7 | content: string; 8 | partial: boolean; 9 | } 10 | 11 | export const toolUseNames = [ 12 | "execute_command", 13 | "read_file", 14 | "write_to_file", 15 | "replace_in_file", 16 | "search_files", 17 | "list_files", 18 | "list_code_definition_names", 19 | "browser_action", 20 | "use_mcp_tool", 21 | "access_mcp_resource", 22 | "ask_followup_question", 23 | "attempt_completion" 24 | ] as const; 25 | 26 | // Converts array of tool call names into a union type ("execute_command" | "read_file" | ...) 27 | export type ToolUseName = (typeof toolUseNames)[number]; 28 | 29 | export const toolParamNames = [ 30 | "command", 31 | "requires_approval", 32 | "path", 33 | "content", 34 | "diff", 35 | "regex", 36 | "file_pattern", 37 | "recursive", 38 | "action", 39 | "url", 40 | "coordinate", 41 | "text", 42 | "server_name", 43 | "tool_name", 44 | "arguments", 45 | "uri", 46 | "question", 47 | "result" 48 | ] as const; 49 | 50 | export type ToolParamName = (typeof toolParamNames)[number]; 51 | 52 | export interface ToolUse { 53 | type: "tool_use"; 54 | name: ToolUseName; 55 | // params is a partial record, allowing only some or none of the possible parameters to be used 56 | params: Partial>; 57 | partial: boolean; 58 | } 59 | 60 | export interface ExecuteCommandToolUse extends ToolUse { 61 | name: "execute_command"; 62 | // Pick, "command"> makes "command" required, but Partial<> makes it optional 63 | params: Partial, "command" | "requires_approval">>; 64 | } 65 | 66 | export interface ReadFileToolUse extends ToolUse { 67 | name: "read_file"; 68 | params: Partial, "path">>; 69 | } 70 | 71 | export interface WriteToFileToolUse extends ToolUse { 72 | name: "write_to_file"; 73 | params: Partial, "path" | "content">>; 74 | } 75 | 76 | export interface ReplaceInFileToolUse extends ToolUse { 77 | name: "replace_in_file"; 78 | params: Partial, "path" | "diff">>; 79 | } 80 | 81 | export interface SearchFilesToolUse extends ToolUse { 82 | name: "search_files"; 83 | params: Partial, "path" | "regex" | "file_pattern">>; 84 | } 85 | 86 | export interface ListFilesToolUse extends ToolUse { 87 | name: "list_files"; 88 | params: Partial, "path" | "recursive">>; 89 | } 90 | 91 | export interface ListCodeDefinitionNamesToolUse extends ToolUse { 92 | name: "list_code_definition_names"; 93 | params: Partial, "path">>; 94 | } 95 | 96 | export interface BrowserActionToolUse extends ToolUse { 97 | name: "browser_action"; 98 | params: Partial, "action" | "url" | "coordinate" | "text">>; 99 | } 100 | 101 | export interface UseMcpToolToolUse extends ToolUse { 102 | name: "use_mcp_tool"; 103 | params: Partial, "server_name" | "tool_name" | "arguments">>; 104 | } 105 | 106 | export interface AccessMcpResourceToolUse extends ToolUse { 107 | name: "access_mcp_resource"; 108 | params: Partial, "server_name" | "uri">>; 109 | } 110 | 111 | export interface AskFollowupQuestionToolUse extends ToolUse { 112 | name: "ask_followup_question"; 113 | params: Partial, "question">>; 114 | } 115 | 116 | export interface AttemptCompletionToolUse extends ToolUse { 117 | name: "attempt_completion"; 118 | params: Partial, "result" | "command">>; 119 | } 120 | -------------------------------------------------------------------------------- /src/extension/core/mentions/MentionParser.ts: -------------------------------------------------------------------------------- 1 | import type { Mention } from "./types"; 2 | 3 | import { InvalidMentionError, MentionType } from "./types"; 4 | 5 | 6 | /** 7 | * Regex for matching mentions while avoiding TypeScript path aliases: 8 | * - Negative lookbehind (? { 88 | if (mention.startsWith("http")) { 89 | return `'${mention}' (see below for site content)`; 90 | } 91 | else if (mention.startsWith("/")) { 92 | const mentionPath = mention.slice(1); 93 | return mentionPath.endsWith("/") 94 | ? `'${mentionPath}' (see below for folder content)` 95 | : `'${mentionPath}' (see below for file content)`; 96 | } 97 | else if (mention === "problems") { 98 | return `Workspace Problems (see below for diagnostics)`; 99 | } 100 | return match; 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/extension/core/mentions/index.ts: -------------------------------------------------------------------------------- 1 | import type { Mention } from "./types"; 2 | 3 | import * as vscode from "vscode"; 4 | 5 | import { MentionType } from "./types"; 6 | import { MentionParser } from "./MentionParser"; 7 | import { MentionContentFetcher } from "./MentionContentFetcher"; 8 | 9 | 10 | /** 11 | * Opens a mention in the appropriate context (file, folder, URL, etc.) 12 | */ 13 | export function openMention(mention?: string): void { 14 | if (mention == null || mention.length === 0) { 15 | return; 16 | } 17 | 18 | // Parse the raw mention to get its structured form 19 | try { 20 | const mentions = MentionParser.parseMentions(`@${mention}`); 21 | if (mentions.length === 0) { 22 | return; 23 | } 24 | 25 | const parsedMention = mentions[0]; 26 | switch (parsedMention.type) { 27 | case MentionType.File: 28 | case MentionType.Folder: 29 | openFileSystemMention(parsedMention); 30 | break; 31 | 32 | case MentionType.Problems: 33 | vscode.commands.executeCommand("workbench.actions.view.problems"); 34 | break; 35 | 36 | case MentionType.Url: 37 | vscode.env.openExternal(vscode.Uri.parse(parsedMention.value)); 38 | break; 39 | } 40 | } 41 | catch (error) { 42 | const message = error instanceof Error ? error.message : String(error); 43 | console.error(`Failed to open mention: ${message}`); 44 | } 45 | } 46 | 47 | /** 48 | * Opens a file system mention (file or folder) 49 | */ 50 | function openFileSystemMention(mention: Mention): void { 51 | const cwd = vscode.workspace.workspaceFolders?.map(folder => folder.uri.fsPath)[0]; 52 | if (cwd == null || cwd.length === 0) { 53 | return; 54 | } 55 | 56 | const absPath = vscode.Uri.file(vscode.Uri.joinPath(vscode.Uri.file(cwd), mention.value).fsPath); 57 | 58 | if (mention.type === MentionType.Folder) { 59 | vscode.commands.executeCommand("revealInExplorer", absPath); 60 | } 61 | else { 62 | vscode.commands.executeCommand("vscode.open", absPath); 63 | } 64 | } 65 | 66 | /** 67 | * Parses mentions in text and fetches their content 68 | */ 69 | export async function parseMentions(text: string, cwd: string): Promise { 70 | // Parse all mentions from text 71 | const mentions = MentionParser.parseMentions(text); 72 | if (mentions.length === 0) { 73 | return text; 74 | } 75 | 76 | // Replace mentions with readable labels 77 | let parsedText = MentionParser.replaceMentionsWithLabels(text); 78 | 79 | // Fetch content for all mentions 80 | const contentFetcher = new MentionContentFetcher(cwd); 81 | const contents = await contentFetcher.fetchContent(mentions); 82 | 83 | // Append each mention's content 84 | for (const { mention, content } of contents) { 85 | switch (mention.type) { 86 | case MentionType.Url: 87 | parsedText += `\n\n\n${content}\n`; 88 | break; 89 | 90 | case MentionType.File: 91 | parsedText += `\n\n\n${content}\n`; 92 | break; 93 | 94 | case MentionType.Folder: 95 | parsedText += `\n\n\n${content}\n`; 96 | break; 97 | 98 | case MentionType.Problems: 99 | parsedText += `\n\n\n${content}\n`; 100 | break; 101 | } 102 | } 103 | 104 | return parsedText; 105 | } 106 | -------------------------------------------------------------------------------- /src/extension/core/mentions/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types of mentions supported by the system 3 | */ 4 | export enum MentionType { 5 | File = "file", 6 | Folder = "folder", 7 | Problems = "problems", 8 | Url = "url" 9 | } 10 | 11 | /** 12 | * Represents a parsed mention 13 | */ 14 | export interface Mention { 15 | type: MentionType; 16 | value: string; 17 | raw: string; 18 | } 19 | 20 | /** 21 | * Content fetched for a mention 22 | */ 23 | export interface MentionContent { 24 | mention: Mention; 25 | content: string; 26 | error?: Error; 27 | } 28 | 29 | /** 30 | * Result of parsing and fetching mention content 31 | */ 32 | export interface ParsedMentionsResult { 33 | text: string; 34 | mentions: MentionContent[]; 35 | } 36 | 37 | /** 38 | * Custom error types for mention handling 39 | */ 40 | export class MentionError extends Error { 41 | constructor(message: string, public readonly code: string) { 42 | super(message); 43 | this.name = "MentionError"; 44 | } 45 | } 46 | 47 | export class FileAccessError extends MentionError { 48 | constructor(message: string) { 49 | super(message, "FILE_ACCESS_ERROR"); 50 | } 51 | } 52 | 53 | export class UrlFetchError extends MentionError { 54 | constructor(message: string) { 55 | super(message, "URL_FETCH_ERROR"); 56 | } 57 | } 58 | 59 | export class InvalidMentionError extends MentionError { 60 | constructor(message: string) { 61 | super(message, "INVALID_MENTION"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/extension/core/prompts/user-system.ts: -------------------------------------------------------------------------------- 1 | export function USER_SYSTEM_PROMPT(settingsCustomInstructions?: string, reclineRulesFileInstructions?: string): string { 2 | return `==== 3 | 4 | ADDITIONAL INSTRUCTIONS 5 | 6 | In addition to all previously specified instructions, the following instructions are provided by the user: 7 | 8 | ${settingsCustomInstructions} 9 | ${reclineRulesFileInstructions} 10 | 11 | `; 12 | } 13 | -------------------------------------------------------------------------------- /src/extension/core/sliding-window/index.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | 4 | /* 5 | We can't implement a dynamically updating sliding window as it would break prompt cache 6 | every time. To maintain the benefits of caching, we need to keep conversation history 7 | static. This operation should be performed as infrequently as possible. If a user reaches 8 | a 200k context, we can assume that the first half is likely irrelevant to their current task. 9 | Therefore, this function should only be called when absolutely necessary to fit within 10 | context limits, not as a continuous process. 11 | */ 12 | export function truncateHalfConversation( 13 | messages: Anthropic.Messages.MessageParam[] 14 | ): Anthropic.Messages.MessageParam[] { 15 | // API expects messages to be in user-assistant order, and tool use messages must be followed by tool results. We need to maintain this structure while truncating. 16 | 17 | // Always keep the first Task message (this includes the project's file structure in environment_details) 18 | const truncatedMessages = [messages[0]]; 19 | 20 | // Remove half of user-assistant pairs 21 | const messagesToRemove = Math.floor(messages.length / 4) * 2; // has to be even number 22 | 23 | const remainingMessages = messages.slice(messagesToRemove + 1); // has to start with assistant message since tool result cannot follow assistant message with no tool use 24 | truncatedMessages.push(...remainingMessages); 25 | 26 | return truncatedMessages; 27 | } 28 | -------------------------------------------------------------------------------- /src/extension/core/webview/getNonce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper function that returns a unique alphanumeric identifier called a nonce. 3 | * 4 | * @remarks This function is primarily used to help enforce content security 5 | * policies for resources/scripts being executed in a webview context. 6 | * 7 | * @returns A nonce 8 | */ 9 | export function getNonce() { 10 | let text = ""; 11 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 12 | for (let i = 0; i < 32; i++) { 13 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 14 | } 15 | return text; 16 | } 17 | -------------------------------------------------------------------------------- /src/extension/core/webview/getUri.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | 4 | /** 5 | * A helper function which will get the webview URI of a given file or resource. 6 | * 7 | * @remarks This URI can be used within a webview's HTML as a link to the 8 | * given file/resource. 9 | * 10 | * @param webview A reference to the extension webview 11 | * @param extensionUri The URI of the directory containing the extension 12 | * @param pathList An array of strings representing the path to a file/resource 13 | * @returns A URI pointing to the file/resource 14 | */ 15 | export function getUri(webview: vscode.Webview, extensionUri: vscode.Uri, pathList: string[]): vscode.Uri { 16 | return webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, ...pathList)); 17 | } 18 | -------------------------------------------------------------------------------- /src/extension/exports/README.md: -------------------------------------------------------------------------------- 1 | # Recline API 2 | 3 | The Recline extension exposes an API that can be used by other extensions. To use this API in your extension: 4 | 5 | 1. Copy `src/extension-api/recline.d.ts` to your extension's source directory. 6 | 2. Include `recline.d.ts` in your extension's compilation. 7 | 3. Get access to the API with the following code: 8 | 9 | ```ts 10 | const reclineExtension = vscode.extensions.getExtension("julesmons.recline"); 11 | 12 | if (!reclineExtension?.isActive) { 13 | throw new Error("Recline extension is not activated"); 14 | } 15 | 16 | const recline = reclineExtension.exports; 17 | 18 | if (recline) { 19 | // Now you can use the API 20 | 21 | // Set custom instructions 22 | await recline.setCustomInstructions("Talk like a pirate"); 23 | 24 | // Get custom instructions 25 | const instructions = await recline.getCustomInstructions(); 26 | console.log("Current custom instructions:", instructions); 27 | 28 | // Start a new task with an initial message 29 | await recline.startNewTask("Hello, Recline! Let's make a new project..."); 30 | 31 | // Start a new task with an initial message and images 32 | await recline.startNewTask("Use this design language", ["data:image/webp;base64,..."]); 33 | 34 | // Send a message to the current task 35 | await recline.sendMessage("Can you fix the problems?"); 36 | 37 | // Simulate pressing the primary button in the chat interface (e.g. 'Save' or 'Proceed While Running') 38 | await recline.pressPrimaryButton(); 39 | 40 | // Simulate pressing the secondary button in the chat interface (e.g. 'Reject') 41 | await recline.pressSecondaryButton(); 42 | } 43 | else { 44 | console.error("Recline API is not available"); 45 | } 46 | ``` 47 | **Note:** To ensure that the `julesmons.recline` extension is activated before your extension, add it to the `extensionDependencies` in your `package.json`: 48 | 49 | ```json 50 | { 51 | "extensionDependencies": [ 52 | "julesmons.recline" 53 | ] 54 | } 55 | ``` 56 | 57 | For detailed information on the available methods and their usage, refer to the `recline.d.ts` file. 58 | -------------------------------------------------------------------------------- /src/extension/exports/index.ts: -------------------------------------------------------------------------------- 1 | import type * as vscode from "vscode"; 2 | 3 | import type { ReclineProvider } from "../core/webview/ReclineProvider"; 4 | 5 | import type { ReclineAPI } from "./recline"; 6 | 7 | 8 | export function createReclineAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ReclineProvider): ReclineAPI { 9 | const api: ReclineAPI = { 10 | setCustomInstructions: async (value: string) => { 11 | await sidebarProvider.updateCustomInstructions(value); 12 | outputChannel.appendLine("Custom instructions set"); 13 | }, 14 | 15 | getCustomInstructions: async () => { 16 | return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined; 17 | }, 18 | 19 | startNewTask: async (task?: string, images?: string[]) => { 20 | outputChannel.appendLine("Starting new task"); 21 | await sidebarProvider.clearTask(); 22 | await sidebarProvider.postStateToWebview(); 23 | await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }); 24 | await sidebarProvider.postMessageToWebview({ 25 | type: "invoke", 26 | invoke: "sendMessage", 27 | text: task, 28 | images 29 | }); 30 | outputChannel.appendLine( 31 | `Task started with message: ${task ? `"${task}"` : "undefined"} and ${images?.length || 0} image(s)` 32 | ); 33 | }, 34 | 35 | sendMessage: async (message?: string, images?: string[]) => { 36 | outputChannel.appendLine( 37 | `Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)` 38 | ); 39 | await sidebarProvider.postMessageToWebview({ 40 | type: "invoke", 41 | invoke: "sendMessage", 42 | text: message, 43 | images 44 | }); 45 | }, 46 | 47 | pressPrimaryButton: async () => { 48 | outputChannel.appendLine("Pressing primary button"); 49 | await sidebarProvider.postMessageToWebview({ 50 | type: "invoke", 51 | invoke: "primaryButtonClick" 52 | }); 53 | }, 54 | 55 | pressSecondaryButton: async () => { 56 | outputChannel.appendLine("Pressing secondary button"); 57 | await sidebarProvider.postMessageToWebview({ 58 | type: "invoke", 59 | invoke: "secondaryButtonClick" 60 | }); 61 | } 62 | }; 63 | 64 | return api; 65 | } 66 | -------------------------------------------------------------------------------- /src/extension/exports/recline.d.ts: -------------------------------------------------------------------------------- 1 | export interface ReclineAPI { 2 | /** 3 | * Sets the custom instructions in the global storage. 4 | * @param value The custom instructions to be saved. 5 | */ 6 | setCustomInstructions: (value: string) => Promise; 7 | 8 | /** 9 | * Retrieves the custom instructions from the global storage. 10 | * @returns The saved custom instructions, or undefined if not set. 11 | */ 12 | getCustomInstructions: () => Promise; 13 | 14 | /** 15 | * Starts a new task with an optional initial message and images. 16 | * @param task Optional initial task message. 17 | * @param images Optional array of image data URIs (e.g., "data:image/webp;base64,..."). 18 | */ 19 | startNewTask: (task?: string, images?: string[]) => Promise; 20 | 21 | /** 22 | * Sends a message to the current task. 23 | * @param message Optional message to send. 24 | * @param images Optional array of image data URIs (e.g., "data:image/webp;base64,..."). 25 | */ 26 | sendMessage: (message?: string, images?: string[]) => Promise; 27 | 28 | /** 29 | * Simulates pressing the primary button in the chat interface. 30 | */ 31 | pressPrimaryButton: () => Promise; 32 | 33 | /** 34 | * Simulates pressing the secondary button in the chat interface. 35 | */ 36 | pressSecondaryButton: () => Promise; 37 | } 38 | -------------------------------------------------------------------------------- /src/extension/integrations/diagnostics/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | 3 | import * as vscode from "vscode"; 4 | import { isEqual } from "es-toolkit"; 5 | 6 | 7 | type FileDiagnostics = [vscode.Uri, vscode.Diagnostic[]][]; 8 | 9 | export class DiagnosticsMonitor { 10 | 11 | /** 12 | * Gets a human-readable label for diagnostic severity. 13 | */ 14 | private getSeverityLabel(severity: vscode.DiagnosticSeverity): string { 15 | switch (severity) { 16 | case vscode.DiagnosticSeverity.Error: 17 | return "Error"; 18 | case vscode.DiagnosticSeverity.Warning: 19 | return "Warning"; 20 | case vscode.DiagnosticSeverity.Information: 21 | return "Information"; 22 | case vscode.DiagnosticSeverity.Hint: 23 | return "Hint"; 24 | default: 25 | return "Diagnostic"; 26 | } 27 | } 28 | 29 | /** 30 | * Formats diagnostics into a human-readable string. 31 | */ 32 | public formatDiagnostics( 33 | diagnostics: FileDiagnostics, 34 | severities: vscode.DiagnosticSeverity[], 35 | cwd: string 36 | ): string { 37 | let result = ""; 38 | 39 | for (const [uri, fileDiagnostics] of diagnostics) { 40 | const problems = fileDiagnostics.filter(d => severities.includes(d.severity)); 41 | if (problems.length > 0) { 42 | result += `\n\n${path.relative(cwd, uri.fsPath)}`; 43 | 44 | for (const diagnostic of problems) { 45 | const label = this.getSeverityLabel(diagnostic.severity); 46 | const line = diagnostic.range.start.line + 1; 47 | const source = diagnostic.source != null ? `${diagnostic.source} ` : ""; 48 | result += `\n- [${source}${label}] Line ${line}: ${diagnostic.message}`; 49 | } 50 | } 51 | } 52 | 53 | return result.trim(); 54 | } 55 | 56 | /** 57 | * Gets new diagnostics by comparing old and new states. 58 | */ 59 | public getNewDiagnostics(oldDiagnostics: FileDiagnostics, newDiagnostics: FileDiagnostics): FileDiagnostics { 60 | const newProblems: FileDiagnostics = []; 61 | const oldMap = new Map(oldDiagnostics); 62 | 63 | for (const [uri, newDiags] of newDiagnostics) { 64 | const oldDiags = oldMap.get(uri) || []; 65 | const newProblemsForUri = newDiags.filter( 66 | newDiag => !oldDiags.some(oldDiag => isEqual(oldDiag, newDiag)) 67 | ); 68 | 69 | if (newProblemsForUri.length > 0) { 70 | newProblems.push([uri, newProblemsForUri]); 71 | } 72 | } 73 | 74 | return newProblems; 75 | } 76 | } 77 | 78 | // Create a singleton instance and export it 79 | export const diagnosticsMonitor = new DiagnosticsMonitor(); 80 | -------------------------------------------------------------------------------- /src/extension/integrations/editor/detect-omission.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | 4 | // Pre-compile patterns for better performance 5 | const COMMENT_PATTERNS = [ 6 | /^\s*\/\//, // Single-line comment for most languages 7 | /^\s*#/, // Single-line comment for Python, Ruby, etc. 8 | /^\s*\/\*/, // Multi-line comment opening 9 | /^\s*\*\//, // Multi-line comment closing 10 | /^\s*\*/, // Multi-line comment continuation 11 | /^\s*\{\s*\/\*/, // JSX comment opening 12 | /^\s*/ // HTML comment closing 14 | ]; 15 | 16 | // Extended set of keywords that might indicate code omissions 17 | const OMISSION_KEYWORDS = new Set([ 18 | "remain", 19 | "remains", 20 | "unchanged", 21 | "rest", 22 | "previous", 23 | "existing", 24 | "continue", 25 | "continues", 26 | "same", 27 | "before", 28 | "original", 29 | "skip", 30 | "omit", 31 | "etc", 32 | "...", 33 | "…" // Unicode ellipsis 34 | ]); 35 | 36 | /** 37 | * Detects potential AI-generated code omissions in the given file content. 38 | * @param originalFileContent The original content of the file. 39 | * @param newFileContent The new content of the file to check. 40 | * @returns True if a potential omission is detected, false otherwise. 41 | */ 42 | function detectCodeOmission(originalFileContent: string, newFileContent: string): boolean { 43 | const originalLines = new Set(originalFileContent.split("\n")); 44 | let inMultilineComment = false; 45 | 46 | for (const line of newFileContent.split("\n")) { 47 | const trimmedLine = line.trim(); 48 | const lineLC = line.toLowerCase(); 49 | 50 | // Handle multi-line comment state 51 | if (trimmedLine.includes("/*")) { 52 | inMultilineComment = true; 53 | } 54 | if (trimmedLine.includes("*/")) { 55 | inMultilineComment = false; 56 | } 57 | 58 | // Skip empty lines or lines that exactly match the original 59 | if (!trimmedLine || originalLines.has(line)) { 60 | continue; 61 | } 62 | 63 | // Check if this line is a comment 64 | const isComment 65 | = inMultilineComment || COMMENT_PATTERNS.some(pattern => pattern.test(line)); 66 | 67 | if (isComment) { 68 | // Split into words and check for omission keywords 69 | const words = lineLC.split(/[\s,.;:\-_]+/); 70 | if (words.some(word => OMISSION_KEYWORDS.has(word))) { 71 | return true; 72 | } 73 | 74 | // Check for phrases like "code continues" or "rest of implementation" 75 | if (/(?:code|implementation|function|method|class)\s+(?:continue|remain)s?/.test(lineLC)) { 76 | return true; 77 | } 78 | } 79 | } 80 | 81 | return false; 82 | } 83 | 84 | /** 85 | * Shows a warning in VSCode if a potential code omission is detected. 86 | * @param originalFileContent The original content of the file. 87 | * @param newFileContent The new content of the file to check. 88 | */ 89 | export function showOmissionWarning(originalFileContent: string, newFileContent: string): void { 90 | if (!originalFileContent || !newFileContent) { 91 | return; 92 | } 93 | 94 | if (detectCodeOmission(originalFileContent, newFileContent)) { 95 | vscode.window 96 | .showWarningMessage( 97 | "Potential code truncation detected. This happens when the AI reaches its max output limit.", 98 | "Follow this guide to fix the issue" 99 | ) 100 | .then((selection) => { 101 | if (selection === "Follow this guide to fix the issue") { 102 | vscode.env.openExternal( 103 | vscode.Uri.parse( 104 | "https://github.com/recline/recline/wiki/Troubleshooting-%E2%80%90-Recline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments" 105 | ) 106 | ); 107 | } 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/extension/integrations/misc/export-markdown.ts: -------------------------------------------------------------------------------- 1 | import type { Anthropic } from "@anthropic-ai/sdk"; 2 | 3 | import os from "node:os"; 4 | import * as path from "node:path"; 5 | 6 | import * as vscode from "vscode"; 7 | 8 | 9 | export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) { 10 | // File name 11 | const date = new Date(dateTs); 12 | const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase(); 13 | const day = date.getDate(); 14 | const year = date.getFullYear(); 15 | let hours = date.getHours(); 16 | const minutes = date.getMinutes().toString().padStart(2, "0"); 17 | const seconds = date.getSeconds().toString().padStart(2, "0"); 18 | const ampm = hours >= 12 ? "pm" : "am"; 19 | hours = hours % 12; 20 | hours = hours || 12; // the hour '0' should be '12' 21 | const fileName = `recline_task_${month}-${day}-${year}_${hours}-${minutes}-${seconds}-${ampm}.md`; 22 | 23 | // Generate markdown 24 | const markdownContent = conversationHistory 25 | .map((message) => { 26 | const role = message.role === "user" ? "**User:**" : "**Assistant:**"; 27 | const content = Array.isArray(message.content) 28 | ? message.content.map(block => formatContentBlockToMarkdown(block)).join("\n") 29 | : message.content; 30 | return `${role}\n\n${content}\n\n`; 31 | }) 32 | .join("---\n\n"); 33 | 34 | // Prompt user for save location 35 | const saveUri = await vscode.window.showSaveDialog({ 36 | filters: { Markdown: ["md"] }, 37 | defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)) 38 | }); 39 | 40 | if (saveUri) { 41 | // Write content to the selected location 42 | await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent)); 43 | vscode.window.showTextDocument(saveUri, { preview: true }); 44 | } 45 | } 46 | 47 | export function formatContentBlockToMarkdown( 48 | block: 49 | | Anthropic.TextBlockParam 50 | | Anthropic.ImageBlockParam 51 | | Anthropic.ToolUseBlockParam 52 | | Anthropic.ToolResultBlockParam 53 | // messages: Anthropic.MessageParam[] 54 | ): string { 55 | switch (block.type) { 56 | case "text": 57 | return block.text; 58 | case "image": 59 | return `[Image]`; 60 | case "tool_use": 61 | let input: string; 62 | if (typeof block.input === "object" && block.input !== null) { 63 | input = Object.entries(block.input) 64 | .map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`) 65 | .join("\n"); 66 | } 67 | else { 68 | input = String(block.input); 69 | } 70 | return `[Tool Use: ${block.name}]\n${input}`; 71 | case "tool_result": 72 | // For now we're not doing tool name lookup since we don't use tools anymore 73 | // const toolName = findToolName(block.tool_use_id, messages) 74 | const toolName = "Tool"; 75 | if (typeof block.content === "string") { 76 | return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content}`; 77 | } 78 | else if (Array.isArray(block.content)) { 79 | return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content 80 | .map(contentBlock => formatContentBlockToMarkdown(contentBlock)) 81 | .join("\n")}`; 82 | } 83 | else { 84 | return `[${toolName}${block.is_error ? " (Error)" : ""}]`; 85 | } 86 | default: 87 | return "[Unexpected content type]"; 88 | } 89 | } 90 | 91 | export function findToolName(toolCallId: string, messages: Anthropic.MessageParam[]): string { 92 | for (const message of messages) { 93 | if (Array.isArray(message.content)) { 94 | for (const block of message.content) { 95 | if (block.type === "tool_use" && block.id === toolCallId) { 96 | return block.name; 97 | } 98 | } 99 | } 100 | } 101 | return "Unknown Tool"; 102 | } 103 | -------------------------------------------------------------------------------- /src/extension/integrations/misc/extract-text.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import * as path from "node:path"; 3 | 4 | import mammoth from "mammoth"; 5 | 6 | 7 | // @ts-ignore-next-line 8 | import pdf from "pdf-parse/lib/pdf-parse"; 9 | import { isBinaryFile } from "isbinaryfile"; 10 | 11 | 12 | export async function extractTextFromFile(filePath: string): Promise { 13 | try { 14 | await fs.access(filePath); 15 | } 16 | catch (error) { 17 | throw new Error(`File not found: ${filePath}`); 18 | } 19 | const fileExtension = path.extname(filePath).toLowerCase(); 20 | switch (fileExtension) { 21 | case ".pdf": 22 | return extractTextFromPDF(filePath); 23 | case ".docx": 24 | return extractTextFromDOCX(filePath); 25 | case ".ipynb": 26 | return extractTextFromIPYNB(filePath); 27 | default: 28 | const isBinary = await isBinaryFile(filePath).catch(() => false); 29 | if (!isBinary) { 30 | return fs.readFile(filePath, "utf8"); 31 | } 32 | else { 33 | throw new Error(`Cannot read text for file type: ${fileExtension}`); 34 | } 35 | } 36 | } 37 | 38 | async function extractTextFromPDF(filePath: string): Promise { 39 | const dataBuffer = await fs.readFile(filePath); 40 | const data = await pdf(dataBuffer); 41 | return data.text; 42 | } 43 | 44 | async function extractTextFromDOCX(filePath: string): Promise { 45 | const result = await mammoth.extractRawText({ path: filePath }); 46 | return result.value; 47 | } 48 | 49 | async function extractTextFromIPYNB(filePath: string): Promise { 50 | const data = await fs.readFile(filePath, "utf8"); 51 | const notebook = JSON.parse(data); 52 | let extractedText = ""; 53 | 54 | for (const cell of notebook.cells) { 55 | if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) { 56 | extractedText += `${cell.source.join("\n")}\n`; 57 | } 58 | } 59 | 60 | return extractedText; 61 | } 62 | -------------------------------------------------------------------------------- /src/extension/integrations/misc/open-file.ts: -------------------------------------------------------------------------------- 1 | import * as os from "node:os"; 2 | import * as path from "node:path"; 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import { arePathsEqual } from "../../utils/path"; 7 | 8 | 9 | export async function openImage(dataUri: string) { 10 | const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/); 11 | if (!matches) { 12 | vscode.window.showErrorMessage("Invalid data URI format"); 13 | return; 14 | } 15 | const [, format, base64Data] = matches; 16 | const imageBuffer = Buffer.from(base64Data, "base64"); 17 | const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`); 18 | try { 19 | await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer); 20 | await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath)); 21 | } 22 | catch (error) { 23 | vscode.window.showErrorMessage(`Error opening image: ${error}`); 24 | } 25 | } 26 | 27 | export async function openFile(absolutePath: string) { 28 | try { 29 | const uri = vscode.Uri.file(absolutePath); 30 | 31 | // Check if the document is already open in a tab group that's not in the active editor's column. If it is, then close it (if not dirty) so that we don't duplicate tabs 32 | try { 33 | for (const group of vscode.window.tabGroups.all) { 34 | const existingTab = group.tabs.find( 35 | tab => 36 | tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, uri.fsPath) 37 | ); 38 | if (existingTab) { 39 | const activeColumn = vscode.window.activeTextEditor?.viewColumn; 40 | const tabColumn = vscode.window.tabGroups.all.find(group => 41 | group.tabs.includes(existingTab) 42 | )?.viewColumn; 43 | if ((activeColumn != null) && activeColumn !== tabColumn && !existingTab.isDirty) { 44 | await vscode.window.tabGroups.close(existingTab); 45 | } 46 | break; 47 | } 48 | } 49 | } 50 | catch {} // not essential, sometimes tab operations fail 51 | 52 | const document = await vscode.workspace.openTextDocument(uri); 53 | await vscode.window.showTextDocument(document, { preview: false }); 54 | } 55 | catch (error) { 56 | vscode.window.showErrorMessage(`Could not open file!`); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/extension/integrations/misc/process-images.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import * as path from "node:path"; 3 | 4 | import * as vscode from "vscode"; 5 | 6 | 7 | export async function selectImages(): Promise { 8 | const options: vscode.OpenDialogOptions = { 9 | canSelectMany: true, 10 | openLabel: "Select", 11 | filters: { 12 | Images: ["png", "jpg", "jpeg", "webp"] // supported by anthropic and openrouter 13 | } 14 | }; 15 | 16 | const fileUris = await vscode.window.showOpenDialog(options); 17 | 18 | if (!fileUris || fileUris.length === 0) { 19 | return []; 20 | } 21 | 22 | return Promise.all( 23 | fileUris.map(async (uri) => { 24 | const imagePath = uri.fsPath; 25 | const buffer = await fs.readFile(imagePath); 26 | const base64 = buffer.toString("base64"); 27 | const mimeType = getMimeType(imagePath); 28 | const dataUrl = `data:${mimeType};base64,${base64}`; 29 | return dataUrl; 30 | }) 31 | ); 32 | } 33 | 34 | function getMimeType(filePath: string): string { 35 | const ext = path.extname(filePath).toLowerCase(); 36 | switch (ext) { 37 | case ".png": 38 | return "image/png"; 39 | case ".jpeg": 40 | case ".jpg": 41 | return "image/jpeg"; 42 | case ".webp": 43 | return "image/webp"; 44 | default: 45 | throw new Error(`Unsupported file type: ${ext}`); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/extension/integrations/notifications/README.md: -------------------------------------------------------------------------------- 1 | # VS Code Notifications 2 | 3 | This module provides a consistent interface for showing VS Code notifications following the [VS Code Notification Guidelines](https://code.visualstudio.com/api/ux-guidelines/notifications). 4 | 5 | ## Basic Usage 6 | 7 | ```typescript 8 | import { showError, showInfo, showWarning } from "./notifications"; 9 | 10 | // Simple information message 11 | await showInfo({ 12 | message: "Operation completed successfully" 13 | }); 14 | 15 | // Warning with title 16 | await showWarning({ 17 | title: "Configuration", 18 | message: "Some settings need to be updated" 19 | }); 20 | 21 | // Error with actions 22 | const result = await showError({ 23 | title: "Build Failed", 24 | message: "Unable to compile project", 25 | actions: ["Retry", "Show Logs", "Cancel"] 26 | }); 27 | 28 | // Handle user action 29 | if (result === "Retry") { 30 | // Handle retry 31 | } 32 | else if (result === "Show Logs") { 33 | // Show logs 34 | } 35 | ``` 36 | 37 | ## Modal Dialogs 38 | 39 | Use modal dialogs when immediate user attention is required: 40 | 41 | ```typescript 42 | const result = await showWarning({ 43 | title: "File Modified", 44 | message: "Save changes before closing?", 45 | actions: ["Save", "Don't Save", "Cancel"], 46 | modal: true 47 | }); 48 | ``` 49 | 50 | ## Progress Notifications 51 | 52 | Show progress for long-running operations: 53 | 54 | ```typescript 55 | await withProgress("Building project", async (progress) => { 56 | progress.report({ message: "Compiling..." }); 57 | await compile(); 58 | 59 | progress.report({ message: "Running tests..." }); 60 | await runTests(); 61 | 62 | return buildResult; 63 | }); 64 | ``` 65 | 66 | ## Input and Selection 67 | 68 | Get user input: 69 | 70 | ```typescript 71 | // Text input 72 | const name = await showInputBox({ 73 | prompt: "Enter your name", 74 | placeHolder: "John Doe" 75 | }); 76 | 77 | // Quick pick selection 78 | const choice = await showQuickPick( 79 | ["Option 1", "Option 2", "Option 3"], 80 | { placeHolder: "Select an option" } 81 | ); 82 | ``` 83 | 84 | ## Best Practices 85 | 86 | 1. Use notifications sparingly to respect user attention 87 | 2. Keep messages clear and concise 88 | 3. Use appropriate notification types: 89 | - Information: Successful operations, neutral information 90 | - Warning: Important messages that need attention 91 | - Error: Failed operations, critical issues 92 | 4. Provide actions when the user can take meaningful steps 93 | 5. Use modal dialogs only when immediate user input is required 94 | 6. Keep progress notifications updated with specific status messages 95 | 7. Always handle action results appropriately 96 | -------------------------------------------------------------------------------- /src/extension/integrations/notifications/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export interface NotificationOptions { 4 | /** The notification message */ 5 | message: string; 6 | /** Optional title to display before the message */ 7 | title?: string; 8 | /** Optional array of actions to display */ 9 | actions?: string[]; 10 | /** Optional modal parameter to force user interaction */ 11 | modal?: boolean; 12 | } 13 | 14 | function formatMessage(title: string | undefined, message: string): string { 15 | if (title === undefined || title === null) { 16 | return message; 17 | } 18 | 19 | if (typeof title !== "string") { 20 | return message; 21 | } 22 | 23 | const trimmedTitle = title.trim(); 24 | if (trimmedTitle.length === 0) { 25 | return message; 26 | } 27 | 28 | return `${trimmedTitle}: ${message}`; 29 | } 30 | 31 | /** 32 | * Shows an information message 33 | */ 34 | export async function showInfo(options: NotificationOptions): Promise { 35 | const message = formatMessage(options.title, options.message); 36 | const items = options.actions || []; 37 | 38 | if (options.modal) { 39 | return vscode.window.showInformationMessage(message, { modal: true }, ...items); 40 | } 41 | return vscode.window.showInformationMessage(message, ...items); 42 | } 43 | 44 | /** 45 | * Shows a warning message 46 | */ 47 | export async function showWarning(options: NotificationOptions): Promise { 48 | const message = formatMessage(options.title, options.message); 49 | const items = options.actions || []; 50 | 51 | if (options.modal) { 52 | return vscode.window.showWarningMessage(message, { modal: true }, ...items); 53 | } 54 | return vscode.window.showWarningMessage(message, ...items); 55 | } 56 | 57 | /** 58 | * Shows an error message 59 | */ 60 | export async function showError(options: NotificationOptions): Promise { 61 | const message = formatMessage(options.title, options.message); 62 | const items = options.actions || []; 63 | 64 | if (options.modal) { 65 | return vscode.window.showErrorMessage(message, { modal: true }, ...items); 66 | } 67 | return vscode.window.showErrorMessage(message, ...items); 68 | } 69 | 70 | /** 71 | * Shows a progress notification 72 | */ 73 | export async function withProgress( 74 | title: string, 75 | task: (progress: vscode.Progress<{ message?: string; increment?: number }>) => Promise 76 | ): Promise { 77 | return vscode.window.withProgress( 78 | { 79 | location: vscode.ProgressLocation.Notification, 80 | title, 81 | cancellable: false 82 | }, 83 | task 84 | ); 85 | } 86 | 87 | /** 88 | * Shows a notification with an input box 89 | */ 90 | export async function showInputBox(options: vscode.InputBoxOptions): Promise { 91 | return vscode.window.showInputBox(options); 92 | } 93 | 94 | /** 95 | * Shows a notification with a quick pick selection 96 | */ 97 | export async function showQuickPick( 98 | items: string[] | Thenable, 99 | options?: vscode.QuickPickOptions 100 | ): Promise { 101 | return vscode.window.showQuickPick(items, options); 102 | } 103 | -------------------------------------------------------------------------------- /src/extension/integrations/terminal/TerminalRegistry.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { extensionPath } from "@extension/constants"; 4 | 5 | 6 | export interface TerminalInfo { 7 | terminal: vscode.Terminal; 8 | busy: boolean; 9 | lastCommand: string; 10 | id: number; 11 | } 12 | 13 | // Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output). 14 | // Since we have promises keeping track of terminal processes, we get the added benefit of keep track of busy terminals even after a task is closed. 15 | export class TerminalRegistry { 16 | private static nextTerminalId = 1; 17 | private static terminals: TerminalInfo[] = []; 18 | 19 | static createTerminal(cwd?: string | vscode.Uri | undefined): TerminalInfo { 20 | const terminal = vscode.window.createTerminal({ 21 | cwd, 22 | name: "Recline", 23 | iconPath: vscode.Uri.file( 24 | vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark 25 | ? `${extensionPath}/assets/icons/recline_dark.svg` 26 | : `${extensionPath}/assets/icons/recline_light.svg` 27 | ) 28 | }); 29 | const newInfo: TerminalInfo = { 30 | terminal, 31 | busy: false, 32 | lastCommand: "", 33 | id: this.nextTerminalId++ 34 | }; 35 | this.terminals.push(newInfo); 36 | return newInfo; 37 | } 38 | 39 | static getAllTerminals(): TerminalInfo[] { 40 | this.terminals = this.terminals.filter(t => !this.isTerminalClosed(t.terminal)); 41 | return this.terminals; 42 | } 43 | 44 | static getTerminal(id: number): TerminalInfo | undefined { 45 | const terminalInfo = this.terminals.find(t => t.id === id); 46 | if (terminalInfo && this.isTerminalClosed(terminalInfo.terminal)) { 47 | this.removeTerminal(id); 48 | return undefined; 49 | } 50 | return terminalInfo; 51 | } 52 | 53 | // The exit status of the terminal will be undefined while the terminal is active. (This value is set when onDidCloseTerminal is fired.) 54 | private static isTerminalClosed(terminal: vscode.Terminal): boolean { 55 | return terminal.exitStatus !== undefined; 56 | } 57 | 58 | static removeTerminal(id: number): void { 59 | this.terminals = this.terminals.filter(t => t.id !== id); 60 | } 61 | 62 | static updateTerminal(id: number, updates: Partial): void { 63 | const terminal = this.getTerminal(id); 64 | if (terminal) { 65 | Object.assign(terminal, updates); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/extension/integrations/terminal/types.ts: -------------------------------------------------------------------------------- 1 | export interface TerminalProcessEvents { 2 | line: [line: string]; 3 | continue: []; 4 | completed: []; 5 | error: [error: Error]; 6 | no_shell_integration: []; 7 | } 8 | -------------------------------------------------------------------------------- /src/extension/integrations/workspace/environment-cache.ts: -------------------------------------------------------------------------------- 1 | import type { EnvironmentInfo } from "./get-env-info"; 2 | 3 | import * as vscode from "vscode"; 4 | 5 | import { getEnvironmentInfo } from "./get-env-info"; 6 | 7 | 8 | // Cache duration in milliseconds (30 minutes) 9 | const CACHE_DURATION = 30 * 60 * 1000; 10 | 11 | interface CacheEntry { 12 | timestamp: number; 13 | data: EnvironmentInfo; 14 | } 15 | 16 | let envInfoCache: CacheEntry | null = null; 17 | 18 | // Register configuration change listener 19 | export function registerEnvironmentCacheEvents(context: vscode.ExtensionContext) { 20 | context.subscriptions.push( 21 | vscode.workspace.onDidChangeConfiguration((event) => { 22 | // Invalidate cache if relevant environment settings change 23 | if ( 24 | event.affectsConfiguration("python") // Python extension settings 25 | || event.affectsConfiguration("typescript") // TypeScript settings 26 | || event.affectsConfiguration("npm") // npm settings 27 | || event.affectsConfiguration("yarn") // yarn settings 28 | ) { 29 | invalidateEnvironmentInfoCache(); 30 | } 31 | }) 32 | ); 33 | } 34 | 35 | export function invalidateEnvironmentInfoCache(): void { 36 | envInfoCache = null; 37 | } 38 | 39 | export async function getCachedEnvironmentInfo(): Promise { 40 | const now = Date.now(); 41 | 42 | // Return cached data if it exists and hasn't expired 43 | if (envInfoCache && now - envInfoCache.timestamp < CACHE_DURATION) { 44 | return envInfoCache.data; 45 | } 46 | 47 | // Fetch fresh data 48 | const freshData = await getEnvironmentInfo(); 49 | 50 | // Update cache 51 | envInfoCache = { 52 | timestamp: now, 53 | data: freshData 54 | }; 55 | 56 | return freshData; 57 | } 58 | -------------------------------------------------------------------------------- /src/extension/integrations/workspace/get-env-info.ts: -------------------------------------------------------------------------------- 1 | import { getPythonEnvPath } from "./get-python-env"; 2 | import { getJavaScriptEnvironment } from "./get-js-env"; 3 | 4 | 5 | export interface EnvironmentInfo { 6 | python?: string; 7 | javascript?: { 8 | nodeVersion?: string; 9 | typescript?: { 10 | version: string; 11 | }; 12 | packageManagers?: Array<{ 13 | name: string; 14 | version: string; 15 | globalPackages: string[]; 16 | }>; 17 | }; 18 | } 19 | 20 | /** 21 | * Fetches environment information about Python and JavaScript/TypeScript environments. 22 | * NOTE: For performance reasons, you should use getCachedEnvironmentInfo from environment-cache.ts 23 | * instead of calling this function directly. 24 | */ 25 | export async function getEnvironmentInfo(): Promise { 26 | const [pythonPath, jsEnv] = await Promise.all([ 27 | getPythonEnvPath(), 28 | getJavaScriptEnvironment() 29 | ]); 30 | 31 | return { 32 | ...(pythonPath && { python: pythonPath }), 33 | ...(Object.keys(jsEnv).length > 0 && { javascript: jsEnv }) 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/extension/integrations/workspace/get-js-env.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from "node:util"; 2 | import { exec } from "node:child_process"; 3 | 4 | import * as vscode from "vscode"; 5 | 6 | 7 | const execAsync = promisify(exec); 8 | 9 | interface PackageManager { 10 | name: string; 11 | version: string; 12 | globalPackages: string[]; 13 | } 14 | 15 | export async function getNodeVersion(): Promise { 16 | try { 17 | const { stdout } = await execAsync("node --version"); 18 | return stdout.trim(); 19 | } 20 | catch { 21 | return undefined; 22 | } 23 | } 24 | 25 | export async function getTypeScriptInfo(): Promise<{ version: string } | undefined> { 26 | const tsExtension = vscode.extensions.getExtension("vscode.typescript-language-features"); 27 | 28 | if (!tsExtension) { 29 | return undefined; 30 | } 31 | 32 | try { 33 | const { stdout } = await execAsync("tsc --version"); 34 | return { version: stdout.replace("Version ", "").trim() }; 35 | } 36 | catch { 37 | return undefined; 38 | } 39 | } 40 | 41 | async function getGlobalPackages(command: string): Promise { 42 | try { 43 | const { stdout } = await execAsync(command); 44 | return stdout 45 | .split("\n") 46 | .filter(Boolean) 47 | .map(line => line.trim()); 48 | } 49 | catch { 50 | return []; 51 | } 52 | } 53 | 54 | export async function getPackageManagers(): Promise { 55 | const managers: PackageManager[] = []; 56 | 57 | // Check npm 58 | try { 59 | const { stdout: npmVersion } = await execAsync("npm --version"); 60 | const globalPackages = await getGlobalPackages("npm list -g --depth=0"); 61 | managers.push({ 62 | name: "npm", 63 | version: npmVersion.trim(), 64 | globalPackages 65 | }); 66 | } 67 | catch {} 68 | 69 | // Check yarn 70 | try { 71 | const { stdout: yarnVersion } = await execAsync("yarn --version"); 72 | const globalPackages = await getGlobalPackages("yarn global list --depth=0"); 73 | managers.push({ 74 | name: "yarn", 75 | version: yarnVersion.trim(), 76 | globalPackages 77 | }); 78 | } 79 | catch {} 80 | 81 | // Check pnpm 82 | try { 83 | const { stdout: pnpmVersion } = await execAsync("pnpm --version"); 84 | const globalPackages = await getGlobalPackages("pnpm list -g --depth=0"); 85 | managers.push({ 86 | name: "pnpm", 87 | version: pnpmVersion.trim(), 88 | globalPackages 89 | }); 90 | } 91 | catch {} 92 | 93 | return managers; 94 | } 95 | 96 | export async function getJavaScriptEnvironment() { 97 | const environment: Record = { 98 | nodeVersion: await getNodeVersion(), 99 | typescript: await getTypeScriptInfo(), 100 | packageManagers: await getPackageManagers() 101 | }; 102 | 103 | // Filter out undefined values 104 | return Object.fromEntries( 105 | Object.entries(environment).filter(([_, value]) => value !== undefined) 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/extension/integrations/workspace/get-python-env.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | 4 | /* 5 | Used to get user's current python environment (unnecessary now that we use the IDE's terminal) 6 | ${await (async () => { 7 | try { 8 | const pythonEnvPath = await getPythonEnvPath() 9 | if (pythonEnvPath) { 10 | return `\nPython Environment: ${pythonEnvPath}` 11 | } 12 | } catch {} 13 | return "" 14 | })()} 15 | */ 16 | export async function getPythonEnvPath(): Promise { 17 | const pythonExtension = vscode.extensions.getExtension("ms-python.python"); 18 | 19 | if (!pythonExtension) { 20 | return undefined; 21 | } 22 | 23 | // Ensure the Python extension is activated 24 | if (!pythonExtension.isActive) { 25 | // if the python extension is not active, we can assume the project is not a python project 26 | return undefined; 27 | } 28 | 29 | // Access the Python extension API 30 | const pythonApi = pythonExtension.exports; 31 | // Get the active environment path for the current workspace 32 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 33 | if (!workspaceFolder) { 34 | return undefined; 35 | } 36 | // Get the active python environment path for the current workspace 37 | const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(workspaceFolder.uri); 38 | if (pythonEnv && pythonEnv.path) { 39 | return pythonEnv.path; 40 | } 41 | else { 42 | return undefined; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/extension/services/browser/urlToMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { fetch } from "undici"; 2 | import { unified } from "unified"; 3 | import rehypeParse from "rehype-parse"; 4 | import rehypeRemark from "rehype-remark"; 5 | import sanitizeHtml from "sanitize-html"; 6 | import remarkStringify from "remark-stringify"; 7 | 8 | 9 | export async function urlToMarkdown(url: string): Promise { 10 | const response = await fetch(url); 11 | const content = sanitizeHtml(await response.text()); 12 | 13 | const markdown = await unified() 14 | .use(rehypeParse) 15 | .use(rehypeRemark) 16 | .use(remarkStringify) 17 | .process(content); 18 | 19 | return markdown.toString(); 20 | } 21 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/languageParser.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import Parser from "web-tree-sitter"; 3 | 4 | import { extensionUri } from "@extension/constants"; 5 | 6 | import { supportedQueries } from "./queries"; 7 | import { importTreeSitterWasm } from "./wasm"; 8 | import { extensionToLanguage } from "./supported"; 9 | 10 | 11 | export interface LanguageParser { 12 | parser: Parser; 13 | query: Parser.Query; 14 | } 15 | 16 | export class LanguageParserManager { 17 | 18 | private isInitialized: boolean = false; 19 | private parsers: Map = new Map(); 20 | 21 | private async createParser(ext: string): Promise { 22 | const supportedLanguage = extensionToLanguage.get(ext); 23 | if (!supportedLanguage) { 24 | throw new Error(`Unsupported extension: ${ext}`); 25 | } 26 | 27 | const wasm = await importTreeSitterWasm(supportedLanguage); 28 | const language = await Parser.Language.load(wasm); 29 | 30 | const rawQuery = supportedQueries.get(supportedLanguage); 31 | if (rawQuery == null || rawQuery === "") { 32 | throw new Error(`Unsupported language: ${supportedLanguage}`); 33 | } 34 | 35 | const query = language.query(rawQuery); 36 | const parser = new Parser(); 37 | parser.setLanguage(language); 38 | 39 | return { parser, query }; 40 | } 41 | 42 | private async initialize(): Promise { 43 | 44 | if (this.isInitialized) { 45 | return; 46 | } 47 | 48 | const wasmUrl = await import("web-tree-sitter/tree-sitter.wasm"); 49 | const fileName: string = wasmUrl.default.toString().split("/").pop() ?? "tree-sitter.wasm"; 50 | const wasmPath: vscode.Uri = vscode.Uri.joinPath(extensionUri, "dist", "assets", fileName); 51 | 52 | await Parser.init({ 53 | locateFile(_scriptName: string, _scriptDirectory: string): string { 54 | return wasmPath.fsPath; 55 | } 56 | }); 57 | 58 | this.isInitialized = true; 59 | } 60 | 61 | public async getParser(ext: string): Promise { 62 | if (!ext) { 63 | throw new Error("No source-file extension provided."); 64 | } 65 | 66 | await this.initialize(); 67 | 68 | if (!this.parsers.has(ext)) { 69 | this.parsers.set(ext, await this.createParser(ext)); 70 | } 71 | 72 | return this.parsers.get(ext)!; 73 | } 74 | 75 | public async getParsers(fileExtensions: string[]): Promise> { 76 | await Promise.all(fileExtensions.map(async ext => this.getParser(ext))); 77 | const result = new Map(); 78 | 79 | for (const ext of fileExtensions) { 80 | const parser = this.parsers.get(ext); 81 | if (parser) { 82 | result.set(ext, parser); 83 | } 84 | } 85 | 86 | return result; 87 | } 88 | } 89 | 90 | // Singleton 91 | export const languageParser = new LanguageParserManager(); 92 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/bash.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - function definitions 3 | - comments/documentation 4 | */ 5 | export const bashQuery = ` 6 | ( 7 | (comment)* @doc 8 | . 9 | (function_definition 10 | name: (_) @name) @definition.function 11 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 12 | (#select-adjacent! @doc @definition.function) 13 | ) 14 | `; 15 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/c-sharp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class declarations 3 | - interface declarations 4 | - method declarations 5 | - namespace declarations 6 | */ 7 | export const csharpQuery = ` 8 | (class_declaration 9 | name: (identifier) @name.definition.class 10 | ) @definition.class 11 | 12 | (interface_declaration 13 | name: (identifier) @name.definition.interface 14 | ) @definition.interface 15 | 16 | (method_declaration 17 | name: (identifier) @name.definition.method 18 | ) @definition.method 19 | 20 | (namespace_declaration 21 | name: (identifier) @name.definition.module 22 | ) @definition.module 23 | `; 24 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/c.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - struct declarations 3 | - union declarations 4 | - function declarations 5 | - typedef declarations 6 | */ 7 | export const cQuery = ` 8 | (struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class 9 | 10 | (declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class 11 | 12 | (function_declarator declarator: (identifier) @name.definition.function) @definition.function 13 | 14 | (type_definition declarator: (type_identifier) @name.definition.type) @definition.type 15 | `; 16 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/cpp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - struct declarations 3 | - union declarations 4 | - function declarations 5 | - method declarations (with namespace scope) 6 | - typedef declarations 7 | - class declarations 8 | */ 9 | export const cppQuery = ` 10 | (struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class 11 | 12 | (declaration type: (union_specifier name: (type_identifier) @name.definition.class)) @definition.class 13 | 14 | (function_declarator declarator: (identifier) @name.definition.function) @definition.function 15 | 16 | (function_declarator declarator: (field_identifier) @name.definition.function) @definition.function 17 | 18 | (function_declarator declarator: (qualified_identifier scope: (namespace_identifier) @scope name: (identifier) @name.definition.method)) @definition.method 19 | 20 | (type_definition declarator: (type_identifier) @name.definition.type) @definition.type 21 | 22 | (class_specifier name: (type_identifier) @name.definition.class) @definition.class 23 | `; 24 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/css.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - rule sets 3 | - media queries 4 | - keyframe definitions 5 | - custom property definitions 6 | */ 7 | export const cssQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (rule_set 12 | (selectors 13 | (class_selector) @name)) @definition.rule 14 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 15 | (#select-adjacent! @doc @definition.rule) 16 | ) 17 | 18 | ( 19 | (comment)* @doc 20 | . 21 | (media_statement) @definition.media 22 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 23 | (#select-adjacent! @doc @definition.media) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (keyframe_block_list 30 | (keyframe_selector) @name) @definition.keyframe 31 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 32 | (#select-adjacent! @doc @definition.keyframe) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (declaration 39 | (property_name) @name 40 | (plain_value 41 | (function_name) @value)) @definition.custom 42 | (#match? @name "^--") 43 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 44 | (#select-adjacent! @doc @definition.custom) 45 | ) 46 | `; 47 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/elisp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - function definitions 3 | - macro definitions 4 | - special forms 5 | */ 6 | export const elispQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | (function_definition 11 | name: (_) @name) @definition.function 12 | (#strip! @doc "^[\\s;]+|^[\\s;]$") 13 | (#select-adjacent! @doc @definition.function) 14 | ) 15 | 16 | ( 17 | (comment)* @doc 18 | . 19 | (macro_definition 20 | name: (_) @name) @definition.macro 21 | (#strip! @doc "^[\\s;]+|^[\\s;]$") 22 | (#select-adjacent! @doc @definition.macro) 23 | ) 24 | 25 | ( 26 | (comment)* @doc 27 | . 28 | (special_form 29 | name: (_) @name) @definition.special 30 | (#strip! @doc "^[\\s;]+|^[\\s;]$") 31 | (#select-adjacent! @doc @definition.special) 32 | ) 33 | `; 34 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/elixir.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - module definitions 3 | - function definitions 4 | - macro definitions 5 | */ 6 | export const elixirQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | (module 11 | name: (_) @name) @definition.module 12 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 13 | (#select-adjacent! @doc @definition.module) 14 | ) 15 | 16 | ( 17 | (comment)* @doc 18 | . 19 | (function 20 | name: (_) @name) @definition.function 21 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 22 | (#select-adjacent! @doc @definition.function) 23 | ) 24 | 25 | ( 26 | (comment)* @doc 27 | . 28 | (macro 29 | name: (_) @name) @definition.macro 30 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 31 | (#select-adjacent! @doc @definition.macro) 32 | ) 33 | `; 34 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/elm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - module declarations 3 | - type definitions 4 | - function definitions 5 | */ 6 | export const elmQuery = ` 7 | ( 8 | (line_comment)* @doc 9 | . 10 | (module_declaration 11 | name: (_) @name) @definition.module 12 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 13 | (#select-adjacent! @doc @definition.module) 14 | ) 15 | 16 | ( 17 | (line_comment)* @doc 18 | . 19 | (type_declaration 20 | name: (_) @name) @definition.type 21 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 22 | (#select-adjacent! @doc @definition.type) 23 | ) 24 | 25 | ( 26 | (line_comment)* @doc 27 | . 28 | (function_declaration_left 29 | name: (_) @name) @definition.function 30 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 31 | (#select-adjacent! @doc @definition.function) 32 | ) 33 | 34 | ( 35 | (line_comment)* @doc 36 | . 37 | (port_declaration 38 | name: (_) @name) @definition.port 39 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 40 | (#select-adjacent! @doc @definition.port) 41 | ) 42 | `; 43 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/embedded_template.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - block definitions 3 | - include statements 4 | - macro/function definitions 5 | - custom tag definitions 6 | */ 7 | export const embedded_templateQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (block 12 | name: (_) @name) @definition.block 13 | (#strip! @doc "^[\\s{%-]+|^[\\s-%}]$") 14 | (#select-adjacent! @doc @definition.block) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (include_statement 21 | path: (_) @name) @definition.include 22 | (#strip! @doc "^[\\s{%-]+|^[\\s-%}]$") 23 | (#select-adjacent! @doc @definition.include) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (macro_definition 30 | name: (_) @name) @definition.macro 31 | (#strip! @doc "^[\\s{%-]+|^[\\s-%}]$") 32 | (#select-adjacent! @doc @definition.macro) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (function_definition 39 | name: (_) @name) @definition.function 40 | (#strip! @doc "^[\\s{%-]+|^[\\s-%}]$") 41 | (#select-adjacent! @doc @definition.function) 42 | ) 43 | 44 | ( 45 | (comment)* @doc 46 | . 47 | (partial_statement 48 | path: (_) @name) @definition.partial 49 | (#strip! @doc "^[\\s{%-]+|^[\\s-%}]$") 50 | (#select-adjacent! @doc @definition.partial) 51 | ) 52 | 53 | ( 54 | (comment)* @doc 55 | . 56 | (custom_tag 57 | name: (_) @name) @definition.tag 58 | (#strip! @doc "^[\\s{%-]+|^[\\s-%}]$") 59 | (#select-adjacent! @doc @definition.tag) 60 | ) 61 | `; 62 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/go.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - function declarations (with associated comments) 3 | - method declarations (with associated comments) 4 | - type specifications 5 | */ 6 | export const goQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | (function_declaration 11 | name: (identifier) @name.definition.function) @definition.function 12 | (#strip! @doc "^//\\s*") 13 | (#set-adjacent! @doc @definition.function) 14 | ) 15 | 16 | ( 17 | (comment)* @doc 18 | . 19 | (method_declaration 20 | name: (field_identifier) @name.definition.method) @definition.method 21 | (#strip! @doc "^//\\s*") 22 | (#set-adjacent! @doc @definition.method) 23 | ) 24 | 25 | (type_spec 26 | name: (type_identifier) @name.definition.type) @definition.type 27 | `; 28 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/html.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - custom elements 3 | - elements with id attributes 4 | - semantic elements (header, nav, main, etc) 5 | */ 6 | export const htmlQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | (element 11 | (start_tag 12 | (tag_name) @name 13 | (attribute 14 | (attribute_name) @attr 15 | (quoted_attribute_value) @value))) @definition.element 16 | (#match? @name "^[A-Z]") // Match custom elements (typically capitalized) 17 | (#strip! @doc "^[\\s]+$") 18 | (#select-adjacent! @doc @definition.element) 19 | ) 20 | 21 | ( 22 | (comment)* @doc 23 | . 24 | (element 25 | (start_tag 26 | (tag_name) @name 27 | (attribute 28 | (attribute_name) @attr 29 | (quoted_attribute_value) @value))) @definition.element 30 | (#eq? @attr "id") // Match elements with ID 31 | (#strip! @doc "^[\\s]+$") 32 | (#select-adjacent! @doc @definition.element) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (element 39 | (start_tag 40 | (tag_name) @name)) @definition.semantic 41 | (#match? @name "^(header|nav|main|footer|article|section|aside)$") // Match semantic elements 42 | (#strip! @doc "^[\\s]+$") 43 | (#select-adjacent! @doc @definition.semantic) 44 | ) 45 | `; 46 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/index.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedLanguage } from "../supported"; 2 | 3 | import { cQuery } from "./c"; 4 | import { goQuery } from "./go"; 5 | import { qlQuery } from "./ql"; 6 | import { cppQuery } from "./cpp"; 7 | import { cssQuery } from "./css"; 8 | import { elmQuery } from "./elm"; 9 | import { luaQuery } from "./lua"; 10 | import { phpQuery } from "./php"; 11 | import { vueQuery } from "./vue"; 12 | import { zigQuery } from "./zig"; 13 | import { tsxQuery } from "./tsx"; 14 | import { bashQuery } from "./bash"; 15 | import { htmlQuery } from "./html"; 16 | import { javaQuery } from "./java"; 17 | import { jsonQuery } from "./json"; 18 | import { objcQuery } from "./objc"; 19 | import { rubyQuery } from "./ruby"; 20 | import { rustQuery } from "./rust"; 21 | import { tomlQuery } from "./toml"; 22 | import { yamlQuery } from "./yaml"; 23 | import { elispQuery } from "./elisp"; 24 | import { ocamlQuery } from "./ocaml"; 25 | import { scalaQuery } from "./scala"; 26 | import { swiftQuery } from "./swift"; 27 | import { elixirQuery } from "./elixir"; 28 | import { kotlinQuery } from "./kotlin"; 29 | import { pythonQuery } from "./python"; 30 | import { csharpQuery } from "./c-sharp"; 31 | import { tlaplusQuery } from "./tlaplus"; 32 | import { rescriptQuery } from "./rescript"; 33 | import { solidityQuery } from "./solidity"; 34 | import { systemrdlQuery } from "./systemrdl"; 35 | import { javaScriptQuery } from "./javascript"; 36 | import { typescriptQuery } from "./typescript"; 37 | import { embedded_templateQuery } from "./embedded_template"; 38 | 39 | 40 | export const supportedQueries: Map = new Map([ 41 | ["bash", bashQuery], 42 | ["c", cQuery], 43 | ["cpp", cppQuery], 44 | ["c_sharp", csharpQuery], 45 | ["css", cssQuery], 46 | ["elisp", elispQuery], 47 | ["elixir", elixirQuery], 48 | ["elm", elmQuery], 49 | ["embedded_template", embedded_templateQuery], 50 | ["go", goQuery], 51 | ["html", htmlQuery], 52 | ["java", javaQuery], 53 | ["javascript", javaScriptQuery], 54 | ["json", jsonQuery], 55 | ["kotlin", kotlinQuery], 56 | ["lua", luaQuery], 57 | ["objc", objcQuery], 58 | ["ocaml", ocamlQuery], 59 | ["php", phpQuery], 60 | ["python", pythonQuery], 61 | ["ql", qlQuery], 62 | ["rescript", rescriptQuery], 63 | ["ruby", rubyQuery], 64 | ["rust", rustQuery], 65 | ["scala", scalaQuery], 66 | ["solidity", solidityQuery], 67 | ["swift", swiftQuery], 68 | ["systemrdl", systemrdlQuery], 69 | ["tlaplus", tlaplusQuery], 70 | ["toml", tomlQuery], 71 | ["typescript", typescriptQuery], 72 | ["tsx", tsxQuery], 73 | ["vue", vueQuery], 74 | ["yaml", yamlQuery], 75 | ["zig", zigQuery] 76 | ]); 77 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/java.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class declarations 3 | - method declarations 4 | - interface declarations 5 | */ 6 | export const javaQuery = ` 7 | (class_declaration 8 | name: (identifier) @name.definition.class) @definition.class 9 | 10 | (method_declaration 11 | name: (identifier) @name.definition.method) @definition.method 12 | 13 | (interface_declaration 14 | name: (identifier) @name.definition.interface) @definition.interface 15 | `; 16 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/javascript.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class definitions 3 | - method definitions 4 | - named function declarations 5 | - arrow functions and function expressions assigned to variables 6 | */ 7 | export const javaScriptQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (method_definition 12 | name: (property_identifier) @name) @definition.method 13 | (#not-eq? @name "constructor") 14 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 15 | (#select-adjacent! @doc @definition.method) 16 | ) 17 | 18 | ( 19 | (comment)* @doc 20 | . 21 | [ 22 | (class 23 | name: (_) @name) 24 | (class_declaration 25 | name: (_) @name) 26 | ] @definition.class 27 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 28 | (#select-adjacent! @doc @definition.class) 29 | ) 30 | 31 | ( 32 | (comment)* @doc 33 | . 34 | [ 35 | (function_declaration 36 | name: (identifier) @name) 37 | (generator_function_declaration 38 | name: (identifier) @name) 39 | ] @definition.function 40 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 41 | (#select-adjacent! @doc @definition.function) 42 | ) 43 | 44 | ( 45 | (comment)* @doc 46 | . 47 | (lexical_declaration 48 | (variable_declarator 49 | name: (identifier) @name 50 | value: [(arrow_function) (function_expression)]) @definition.function) 51 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 52 | (#select-adjacent! @doc @definition.function) 53 | ) 54 | 55 | ( 56 | (comment)* @doc 57 | . 58 | (variable_declaration 59 | (variable_declarator 60 | name: (identifier) @name 61 | value: [(arrow_function) (function_expression)]) @definition.function) 62 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 63 | (#select-adjacent! @doc @definition.function) 64 | ) 65 | `; 66 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/json.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - object key-value definitions at the root level 3 | */ 4 | export const jsonQuery = ` 5 | ( 6 | (pair 7 | key: (string) @name 8 | value: (object)) @definition.object 9 | ) 10 | `; 11 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/kotlin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class definitions 3 | - function definitions 4 | - property definitions 5 | - interface definitions 6 | */ 7 | export const kotlinQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (class_declaration 12 | name: (_) @name) @definition.class 13 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 14 | (#select-adjacent! @doc @definition.class) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (function_declaration 21 | name: (_) @name) @definition.function 22 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 23 | (#select-adjacent! @doc @definition.function) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (property_declaration 30 | name: (_) @name) @definition.property 31 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 32 | (#select-adjacent! @doc @definition.property) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (interface_declaration 39 | name: (_) @name) @definition.interface 40 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 41 | (#select-adjacent! @doc @definition.interface) 42 | ) 43 | 44 | ( 45 | (comment)* @doc 46 | . 47 | (object_declaration 48 | name: (_) @name) @definition.object 49 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 50 | (#select-adjacent! @doc @definition.object) 51 | ) 52 | `; 53 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/lua.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - function definitions 3 | - table definitions 4 | - requires/imports 5 | */ 6 | export const luaQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | (function_declaration 11 | name: (_) @name) @definition.function 12 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 13 | (#select-adjacent! @doc @definition.function) 14 | ) 15 | 16 | ( 17 | (comment)* @doc 18 | . 19 | (function_definition 20 | name: (_) @name) @definition.function 21 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 22 | (#select-adjacent! @doc @definition.function) 23 | ) 24 | 25 | ( 26 | (comment)* @doc 27 | . 28 | (local_function 29 | name: (_) @name) @definition.function 30 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 31 | (#select-adjacent! @doc @definition.function) 32 | ) 33 | 34 | ( 35 | (comment)* @doc 36 | . 37 | (table_constructor) @definition.table 38 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 39 | (#select-adjacent! @doc @definition.table) 40 | ) 41 | 42 | ( 43 | (comment)* @doc 44 | . 45 | (assignment_statement 46 | (variable_list 47 | (identifier) @name) 48 | (expression_list 49 | (function_call 50 | (identifier) @call))) @definition.require 51 | (#eq? @call "require") 52 | (#strip! @doc "^[\\s--]+|^[\\s--]$") 53 | (#select-adjacent! @doc @definition.require) 54 | ) 55 | `; 56 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/objc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - interface declarations 3 | - implementation declarations 4 | - method declarations 5 | - property declarations 6 | */ 7 | export const objcQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (interface_declaration 12 | name: (_) @name) @definition.interface 13 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 14 | (#select-adjacent! @doc @definition.interface) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (implementation_definition 21 | name: (_) @name) @definition.implementation 22 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 23 | (#select-adjacent! @doc @definition.implementation) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (method_definition 30 | (method_selector 31 | (selector_name) @name)) @definition.method 32 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 33 | (#select-adjacent! @doc @definition.method) 34 | ) 35 | 36 | ( 37 | (comment)* @doc 38 | . 39 | (property_declaration 40 | (property_attributes)? 41 | type: (_) 42 | name: (_) @name) @definition.property 43 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 44 | (#select-adjacent! @doc @definition.property) 45 | ) 46 | 47 | ( 48 | (comment)* @doc 49 | . 50 | (category_interface 51 | name: (_) @name 52 | category: (_)) @definition.category 53 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 54 | (#select-adjacent! @doc @definition.category) 55 | ) 56 | `; 57 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/ocaml.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - module definitions 3 | - function definitions 4 | - type definitions 5 | - variant definitions 6 | */ 7 | export const ocamlQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (module_definition 12 | name: (_) @name) @definition.module 13 | (#strip! @doc "^[\\s\\*]+|^[\\s\\*]$") 14 | (#select-adjacent! @doc @definition.module) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (let_binding 21 | pattern: (value_pattern) @name) @definition.function 22 | (#strip! @doc "^[\\s\\*]+|^[\\s\\*]$") 23 | (#select-adjacent! @doc @definition.function) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (type_definition 30 | name: (_) @name) @definition.type 31 | (#strip! @doc "^[\\s\\*]+|^[\\s\\*]$") 32 | (#select-adjacent! @doc @definition.type) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (constructor_declaration 39 | name: (_) @name) @definition.variant 40 | (#strip! @doc "^[\\s\\*]+|^[\\s\\*]$") 41 | (#select-adjacent! @doc @definition.variant) 42 | ) 43 | 44 | ( 45 | (comment)* @doc 46 | . 47 | (external 48 | name: (_) @name) @definition.external 49 | (#strip! @doc "^[\\s\\*]+|^[\\s\\*]$") 50 | (#select-adjacent! @doc @definition.external) 51 | ) 52 | `; 53 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/php.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class declarations 3 | - function definitions 4 | - method declarations 5 | */ 6 | export const phpQuery = ` 7 | (class_declaration 8 | name: (name) @name.definition.class) @definition.class 9 | 10 | (function_definition 11 | name: (name) @name.definition.function) @definition.function 12 | 13 | (method_declaration 14 | name: (name) @name.definition.function) @definition.function 15 | `; 16 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/python.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class definitions 3 | - function definitions 4 | */ 5 | export const pythonQuery = ` 6 | (class_definition 7 | name: (identifier) @name.definition.class) @definition.class 8 | 9 | (function_definition 10 | name: (identifier) @name.definition.function) @definition.function 11 | `; 12 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/ql.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class definitions 3 | - predicate definitions 4 | - query definitions 5 | */ 6 | export const qlQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | (class 11 | name: (_) @name) @definition.class 12 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 13 | (#select-adjacent! @doc @definition.class) 14 | ) 15 | 16 | ( 17 | (comment)* @doc 18 | . 19 | (predicate 20 | name: (_) @name) @definition.predicate 21 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 22 | (#select-adjacent! @doc @definition.predicate) 23 | ) 24 | 25 | ( 26 | (comment)* @doc 27 | . 28 | (select 29 | name: (_) @name) @definition.query 30 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 31 | (#select-adjacent! @doc @definition.query) 32 | ) 33 | 34 | ( 35 | (comment)* @doc 36 | . 37 | (module 38 | name: (_) @name) @definition.module 39 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 40 | (#select-adjacent! @doc @definition.module) 41 | ) 42 | 43 | ( 44 | (comment)* @doc 45 | . 46 | (import 47 | module: (_) @name) @definition.import 48 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 49 | (#select-adjacent! @doc @definition.import) 50 | ) 51 | `; 52 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/rescript.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - module definitions 3 | - type definitions 4 | - function definitions 5 | - external bindings 6 | */ 7 | export const rescriptQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (module_declaration 12 | name: (_) @name) @definition.module 13 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 14 | (#select-adjacent! @doc @definition.module) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (type_declaration 21 | name: (_) @name) @definition.type 22 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 23 | (#select-adjacent! @doc @definition.type) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (let_declaration 30 | name: (_) @name) @definition.function 31 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 32 | (#select-adjacent! @doc @definition.function) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (external_declaration 39 | name: (_) @name) @definition.external 40 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 41 | (#select-adjacent! @doc @definition.external) 42 | ) 43 | 44 | ( 45 | (comment)* @doc 46 | . 47 | (record_declaration 48 | name: (_) @name) @definition.record 49 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 50 | (#select-adjacent! @doc @definition.record) 51 | ) 52 | `; 53 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/ruby.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - method definitions (including singleton methods and aliases, with associated comments) 3 | - class definitions (including singleton classes, with associated comments) 4 | - module definitions 5 | */ 6 | export const rubyQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | [ 11 | (method 12 | name: (_) @name.definition.method) @definition.method 13 | (singleton_method 14 | name: (_) @name.definition.method) @definition.method 15 | ] 16 | (#strip! @doc "^#\\s*") 17 | (#select-adjacent! @doc @definition.method) 18 | ) 19 | 20 | (alias 21 | name: (_) @name.definition.method) @definition.method 22 | 23 | ( 24 | (comment)* @doc 25 | . 26 | [ 27 | (class 28 | name: [ 29 | (constant) @name.definition.class 30 | (scope_resolution 31 | name: (_) @name.definition.class) 32 | ]) @definition.class 33 | (singleton_class 34 | value: [ 35 | (constant) @name.definition.class 36 | (scope_resolution 37 | name: (_) @name.definition.class) 38 | ]) @definition.class 39 | ] 40 | (#strip! @doc "^#\\s*") 41 | (#select-adjacent! @doc @definition.class) 42 | ) 43 | 44 | ( 45 | (module 46 | name: [ 47 | (constant) @name.definition.module 48 | (scope_resolution 49 | name: (_) @name.definition.module) 50 | ]) @definition.module 51 | ) 52 | `; 53 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/rust.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - struct definitions 3 | - method definitions 4 | - function definitions 5 | */ 6 | export const rustQuery = ` 7 | (struct_item 8 | name: (type_identifier) @name.definition.class) @definition.class 9 | 10 | (declaration_list 11 | (function_item 12 | name: (identifier) @name.definition.method)) @definition.method 13 | 14 | (function_item 15 | name: (identifier) @name.definition.function) @definition.function 16 | `; 17 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/scala.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class definitions 3 | - object definitions 4 | - trait definitions 5 | - function definitions 6 | - val/var definitions 7 | */ 8 | export const scalaQuery = ` 9 | ( 10 | (comment)* @doc 11 | . 12 | (class_definition 13 | name: (_) @name) @definition.class 14 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 15 | (#select-adjacent! @doc @definition.class) 16 | ) 17 | 18 | ( 19 | (comment)* @doc 20 | . 21 | (object_definition 22 | name: (_) @name) @definition.object 23 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 24 | (#select-adjacent! @doc @definition.object) 25 | ) 26 | 27 | ( 28 | (comment)* @doc 29 | . 30 | (trait_definition 31 | name: (_) @name) @definition.trait 32 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 33 | (#select-adjacent! @doc @definition.trait) 34 | ) 35 | 36 | ( 37 | (comment)* @doc 38 | . 39 | (function_definition 40 | name: (_) @name) @definition.function 41 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 42 | (#select-adjacent! @doc @definition.function) 43 | ) 44 | 45 | ( 46 | (comment)* @doc 47 | . 48 | (val_definition 49 | pattern: (_) @name) @definition.val 50 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 51 | (#select-adjacent! @doc @definition.val) 52 | ) 53 | 54 | ( 55 | (comment)* @doc 56 | . 57 | (var_definition 58 | pattern: (_) @name) @definition.var 59 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 60 | (#select-adjacent! @doc @definition.var) 61 | ) 62 | `; 63 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/solidity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - contract definitions 3 | - function definitions 4 | - event definitions 5 | - modifier definitions 6 | - struct definitions 7 | */ 8 | export const solidityQuery = ` 9 | ( 10 | (comment)* @doc 11 | . 12 | (contract_declaration 13 | name: (_) @name) @definition.contract 14 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 15 | (#select-adjacent! @doc @definition.contract) 16 | ) 17 | 18 | ( 19 | (comment)* @doc 20 | . 21 | (function_definition 22 | name: (_) @name) @definition.function 23 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 24 | (#select-adjacent! @doc @definition.function) 25 | ) 26 | 27 | ( 28 | (comment)* @doc 29 | . 30 | (event_definition 31 | name: (_) @name) @definition.event 32 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 33 | (#select-adjacent! @doc @definition.event) 34 | ) 35 | 36 | ( 37 | (comment)* @doc 38 | . 39 | (modifier_definition 40 | name: (_) @name) @definition.modifier 41 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 42 | (#select-adjacent! @doc @definition.modifier) 43 | ) 44 | 45 | ( 46 | (comment)* @doc 47 | . 48 | (struct_declaration 49 | name: (_) @name) @definition.struct 50 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 51 | (#select-adjacent! @doc @definition.struct) 52 | ) 53 | 54 | ( 55 | (comment)* @doc 56 | . 57 | (interface_declaration 58 | name: (_) @name) @definition.interface 59 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 60 | (#select-adjacent! @doc @definition.interface) 61 | ) 62 | `; 63 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/swift.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - class declarations 3 | - method declarations (including initializers and deinitializers) 4 | - property declarations 5 | - function declarations 6 | */ 7 | export const swiftQuery = ` 8 | (class_declaration 9 | name: (type_identifier) @name) @definition.class 10 | 11 | (protocol_declaration 12 | name: (type_identifier) @name) @definition.interface 13 | 14 | (class_declaration 15 | (class_body 16 | [ 17 | (function_declaration 18 | name: (simple_identifier) @name 19 | ) 20 | (subscript_declaration 21 | (parameter (simple_identifier) @name) 22 | ) 23 | (init_declaration "init" @name) 24 | (deinit_declaration "deinit" @name) 25 | ] 26 | ) 27 | ) @definition.method 28 | 29 | (class_declaration 30 | (class_body 31 | [ 32 | (property_declaration 33 | (pattern (simple_identifier) @name) 34 | ) 35 | ] 36 | ) 37 | ) @definition.property 38 | 39 | (property_declaration 40 | (pattern (simple_identifier) @name) 41 | ) @definition.property 42 | 43 | (function_declaration 44 | name: (simple_identifier) @name) @definition.function 45 | `; 46 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/systemrdl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - component definitions 3 | - register definitions 4 | - field definitions 5 | - property definitions 6 | */ 7 | export const systemrdlQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (component_definition 12 | name: (_) @name) @definition.component 13 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 14 | (#select-adjacent! @doc @definition.component) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (register_definition 21 | name: (_) @name) @definition.register 22 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 23 | (#select-adjacent! @doc @definition.register) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (field_definition 30 | name: (_) @name) @definition.field 31 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 32 | (#select-adjacent! @doc @definition.field) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (property_definition 39 | name: (_) @name) @definition.property 40 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 41 | (#select-adjacent! @doc @definition.property) 42 | ) 43 | 44 | ( 45 | (comment)* @doc 46 | . 47 | (instance_definition 48 | name: (_) @name) @definition.instance 49 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 50 | (#select-adjacent! @doc @definition.instance) 51 | ) 52 | `; 53 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/tlaplus.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - module definitions 3 | - operator definitions 4 | - theorem declarations 5 | - constant declarations 6 | */ 7 | export const tlaplusQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (module_definition 12 | name: (_) @name) @definition.module 13 | (#strip! @doc "^[\\s\\(*]+|^[\\s\\*)]$") 14 | (#select-adjacent! @doc @definition.module) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (operator_definition 21 | name: (_) @name) @definition.operator 22 | (#strip! @doc "^[\\s\\(*]+|^[\\s\\*)]$") 23 | (#select-adjacent! @doc @definition.operator) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (theorem 30 | name: (_) @name) @definition.theorem 31 | (#strip! @doc "^[\\s\\(*]+|^[\\s\\*)]$") 32 | (#select-adjacent! @doc @definition.theorem) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (constant_declaration 39 | name: (_) @name) @definition.constant 40 | (#strip! @doc "^[\\s\\(*]+|^[\\s\\*)]$") 41 | (#select-adjacent! @doc @definition.constant) 42 | ) 43 | 44 | ( 45 | (comment)* @doc 46 | . 47 | (variable_declaration 48 | name: (_) @name) @definition.variable 49 | (#strip! @doc "^[\\s\\(*]+|^[\\s\\*)]$") 50 | (#select-adjacent! @doc @definition.variable) 51 | ) 52 | `; 53 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/toml.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - table definitions 3 | - array table definitions 4 | - key value pairs 5 | */ 6 | export const tomlQuery = ` 7 | ( 8 | (comment)* @doc 9 | . 10 | (table 11 | (table_array_element) @name) @definition.table_array 12 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 13 | (#select-adjacent! @doc @definition.table_array) 14 | ) 15 | 16 | ( 17 | (comment)* @doc 18 | . 19 | (table 20 | name: (_) @name) @definition.table 21 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 22 | (#select-adjacent! @doc @definition.table) 23 | ) 24 | 25 | ( 26 | (comment)* @doc 27 | . 28 | (pair 29 | key: (_) @name 30 | value: (array)) @definition.array 31 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 32 | (#select-adjacent! @doc @definition.array) 33 | ) 34 | 35 | ( 36 | (comment)* @doc 37 | . 38 | (pair 39 | key: (_) @name 40 | value: (inline_table)) @definition.inline_table 41 | (#strip! @doc "^[\\s#]+|^[\\s#]$") 42 | (#select-adjacent! @doc @definition.inline_table) 43 | ) 44 | `; 45 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/tsx.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - component definitions 3 | - TSX function components 4 | - hook definitions 5 | - JSX/TSX patterns 6 | */ 7 | export const tsxQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (class_declaration 12 | name: (_) @name) @definition.component 13 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 14 | (#select-adjacent! @doc @definition.component) 15 | ) 16 | 17 | ( 18 | (comment)* @doc 19 | . 20 | (function_declaration 21 | name: (identifier) @name) @definition.function 22 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 23 | (#select-adjacent! @doc @definition.function) 24 | ) 25 | 26 | ( 27 | (comment)* @doc 28 | . 29 | (variable_declarator 30 | name: (_) @name 31 | value: [(arrow_function) (function_expression)]) @definition.function 32 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 33 | (#select-adjacent! @doc @definition.function) 34 | ) 35 | 36 | ( 37 | (comment)* @doc 38 | . 39 | (lexical_declaration 40 | (variable_declarator 41 | name: (_) @name 42 | value: [(arrow_function) (function_expression)]) @definition.hook) 43 | (#match? @name "^use[A-Z]") 44 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 45 | (#select-adjacent! @doc @definition.hook) 46 | ) 47 | 48 | ( 49 | (comment)* @doc 50 | . 51 | (interface_declaration 52 | name: (_) @name) @definition.interface 53 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 54 | (#select-adjacent! @doc @definition.interface) 55 | ) 56 | 57 | ( 58 | (comment)* @doc 59 | . 60 | (type_alias_declaration 61 | name: (_) @name) @definition.type 62 | (#strip! @doc "^[\\s//*]+|^[\\s//*]$") 63 | (#select-adjacent! @doc @definition.type) 64 | ) 65 | `; 66 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/typescript.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - function signatures and declarations 3 | - method signatures and definitions 4 | - abstract method signatures 5 | - class declarations (including abstract classes) 6 | - module declarations 7 | */ 8 | export const typescriptQuery = ` 9 | (function_signature 10 | name: (identifier) @name.definition.function) @definition.function 11 | 12 | (method_signature 13 | name: (property_identifier) @name.definition.method) @definition.method 14 | 15 | (abstract_method_signature 16 | name: (property_identifier) @name.definition.method) @definition.method 17 | 18 | (abstract_class_declaration 19 | name: (type_identifier) @name.definition.class) @definition.class 20 | 21 | (module 22 | name: (identifier) @name.definition.module) @definition.module 23 | 24 | (function_declaration 25 | name: (identifier) @name.definition.function) @definition.function 26 | 27 | (method_definition 28 | name: (property_identifier) @name.definition.method) @definition.method 29 | 30 | (class_declaration 31 | name: (type_identifier) @name.definition.class) @definition.class 32 | `; 33 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/vue.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - component definitions 3 | - method definitions 4 | - computed properties 5 | - data properties 6 | */ 7 | export const vueQuery = ` 8 | ( 9 | (comment)* @doc 10 | . 11 | (script_element 12 | (raw_text 13 | (export_statement 14 | (class_declaration 15 | name: (_) @name)))) @definition.component 16 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 17 | (#select-adjacent! @doc @definition.component) 18 | ) 19 | 20 | ( 21 | (comment)* @doc 22 | . 23 | (script_element 24 | (raw_text 25 | (export_statement 26 | declaration: (object 27 | (pair 28 | key: (property_identifier) @name 29 | value: [(function_expression) (arrow_function)]))))) @definition.method 30 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 31 | (#select-adjacent! @doc @definition.method) 32 | ) 33 | 34 | ( 35 | (comment)* @doc 36 | . 37 | (script_element 38 | (raw_text 39 | (export_statement 40 | (object 41 | (method_definition 42 | name: (property_identifier) @name))))) @definition.method 43 | (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$") 44 | (#select-adjacent! @doc @definition.method) 45 | ) 46 | `; 47 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/yaml.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - top-level block mappings 3 | - document-level definitions 4 | */ 5 | export const yamlQuery = ` 6 | ( 7 | (block_mapping_pair 8 | key: (_) @name 9 | value: (block_mapping)) @definition.mapping 10 | ) 11 | 12 | ( 13 | (document 14 | (block_mapping 15 | (block_mapping_pair 16 | key: (_) @name))) @definition.document 17 | ) 18 | `; 19 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/queries/zig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | - function definitions 3 | - struct definitions 4 | - type definitions 5 | - enum definitions 6 | */ 7 | export const zigQuery = ` 8 | ( 9 | (line_comment)* @doc 10 | . 11 | (FnProto 12 | name: (_) @name) @definition.function 13 | (#strip! @doc "^[\\s//]+|^[\\s//]$") 14 | (#select-adjacent! @doc @definition.function) 15 | ) 16 | 17 | ( 18 | (line_comment)* @doc 19 | . 20 | (ContainerDecl 21 | name: (_) @name) @definition.struct 22 | (#strip! @doc "^[\\s//]+|^[\\s//]$") 23 | (#select-adjacent! @doc @definition.struct) 24 | ) 25 | 26 | ( 27 | (line_comment)* @doc 28 | . 29 | (VarDecl 30 | name: (_) @name 31 | type: (_)) @definition.variable 32 | (#strip! @doc "^[\\s//]+|^[\\s//]$") 33 | (#select-adjacent! @doc @definition.variable) 34 | ) 35 | 36 | ( 37 | (line_comment)* @doc 38 | . 39 | (ErrorDecl 40 | name: (_) @name) @definition.error 41 | (#strip! @doc "^[\\s//]+|^[\\s//]$") 42 | (#select-adjacent! @doc @definition.error) 43 | ) 44 | 45 | ( 46 | (line_comment)* @doc 47 | . 48 | (TestDecl 49 | name: (_) @name) @definition.test 50 | (#strip! @doc "^[\\s//]+|^[\\s//]$") 51 | (#select-adjacent! @doc @definition.test) 52 | ) 53 | `; 54 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/supported.ts: -------------------------------------------------------------------------------- 1 | export type SupportedLanguage = 2 | | "bash" 3 | | "c" 4 | | "cpp" 5 | | "c_sharp" 6 | | "css" 7 | | "elisp" 8 | | "elixir" 9 | | "elm" 10 | | "go" 11 | | "html" 12 | | "java" 13 | | "javascript" 14 | | "json" 15 | | "kotlin" 16 | | "lua" 17 | | "objc" 18 | | "ocaml" 19 | | "php" 20 | | "python" 21 | | "ql" 22 | | "rescript" 23 | | "ruby" 24 | | "rust" 25 | | "scala" 26 | | "solidity" 27 | | "swift" 28 | | "systemrdl" 29 | | "tlaplus" 30 | | "toml" 31 | | "tsx" 32 | | "typescript" 33 | | "vue" 34 | | "yaml" 35 | | "zig" 36 | | "embedded_template"; 37 | 38 | export const supportedLanguages: Map = new Map([ 39 | ["bash", ["sh"]], 40 | ["c", ["c", "h"]], 41 | ["cpp", ["cpp", "hpp"]], 42 | ["c_sharp", ["cs"]], 43 | ["css", ["css"]], 44 | ["elisp", ["el"]], 45 | ["elixir", ["ex"]], 46 | ["elm", ["elm"]], 47 | ["go", ["go"]], 48 | ["html", ["html"]], 49 | ["java", ["java"]], 50 | ["javascript", ["js", "jsx"]], 51 | ["json", ["json"]], 52 | ["kotlin", ["kt"]], 53 | ["lua", ["lua"]], 54 | ["objc", ["m"]], 55 | ["ocaml", ["ml"]], 56 | ["php", ["php"]], 57 | ["python", ["py"]], 58 | ["ql", ["ql"]], 59 | ["rescript", ["res"]], 60 | ["ruby", ["rb"]], 61 | ["rust", ["rs"]], 62 | ["scala", ["scala"]], 63 | ["solidity", ["sol"]], 64 | ["swift", ["swift"]], 65 | ["systemrdl", ["systemrdl"]], 66 | ["tlaplus", ["tla"]], 67 | ["toml", ["toml"]], 68 | ["tsx", ["tsx"]], 69 | ["typescript", ["ts"]], 70 | ["vue", ["vue"]], 71 | ["yaml", ["yaml"]], 72 | ["zig", ["zig"]], 73 | ["embedded_template", ["ejs", "jinja", "liquid", "njk", "pug", "twig"]] 74 | ]); 75 | 76 | export const supportedExtensions: string[] = Array.from(supportedLanguages.values()).flat(); 77 | 78 | // Create a reverse mapping of extension -> language for O(1) lookup 79 | export const extensionToLanguage = new Map( 80 | Array.from(supportedLanguages.entries()) 81 | .flatMap(([lang, exts]) => 82 | exts.map(ext => [ext, lang]) 83 | ) 84 | ); 85 | -------------------------------------------------------------------------------- /src/extension/services/tree-sitter/types.ts: -------------------------------------------------------------------------------- 1 | import type Parser from "web-tree-sitter"; 2 | 3 | 4 | /** 5 | * Position in a source code file 6 | */ 7 | export interface TreeSitterPosition { 8 | row: number; 9 | column: number; 10 | } 11 | 12 | /** 13 | * Node in the AST that represents a captured syntax element 14 | */ 15 | export interface TreeSitterNode { 16 | startPosition: TreeSitterPosition; 17 | endPosition: TreeSitterPosition; 18 | text: string; 19 | } 20 | 21 | /** 22 | * Represents a matching capture from a tree-sitter query 23 | */ 24 | export interface CaptureMatch { 25 | name: string; 26 | node: TreeSitterNode; 27 | pattern?: number; 28 | } 29 | 30 | /** 31 | * Result from parsing a file's definitions 32 | */ 33 | export interface DefinitionParseResult { 34 | relativePath: string; 35 | definitions?: string; 36 | } 37 | 38 | /** 39 | * Tree-sitter language-specific parser configuration 40 | */ 41 | export interface TreeSitterParser { 42 | parser: Parser; 43 | query: Parser.Query; 44 | } 45 | -------------------------------------------------------------------------------- /src/extension/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.wasm" { 2 | const content: URL; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /src/extension/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { readFile } from "node:fs/promises"; 3 | 4 | import * as vscode from "vscode"; 5 | import { after, describe, it } from "mocha"; 6 | 7 | import "should"; 8 | 9 | 10 | const packagePath = path.join(__dirname, "..", "..", "..", "package.json"); 11 | 12 | describe("Recline Extension", () => { 13 | after(() => { 14 | vscode.window.showInformationMessage("All tests done!"); 15 | }); 16 | 17 | it("should verify extension ID matches package.json", async () => { 18 | const packageJSON = JSON.parse(await readFile(packagePath, "utf8")); 19 | const id = `${packageJSON.publisher}.${packageJSON.name}`; 20 | const reclineExtensionApi = vscode.extensions.getExtension(id); 21 | 22 | reclineExtensionApi?.id.should.equal(id); 23 | }); 24 | 25 | it("should successfully execute the plus button command", async () => { 26 | await new Promise(resolve => setTimeout(resolve, 400)); 27 | await vscode.commands.executeCommand("recline.plusButtonClicked"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/extension/utils/cost.test.ts: -------------------------------------------------------------------------------- 1 | import type { ModelInfo } from "@shared/api"; 2 | 3 | import { describe, it } from "mocha"; 4 | 5 | import { calculateApiCost } from "./cost"; 6 | 7 | import "should"; 8 | 9 | 10 | describe("Cost Utilities", () => { 11 | describe("calculateApiCost", () => { 12 | it("should calculate basic input/output costs", () => { 13 | const modelInfo: ModelInfo = { 14 | supportsPromptCache: false, 15 | inputPrice: 3.0, // $3 per million tokens 16 | outputPrice: 15.0 // $15 per million tokens 17 | }; 18 | 19 | const cost = calculateApiCost(modelInfo, 1000, 500); 20 | // Input: (3.0 / 1_000_000) * 1000 = 0.003 21 | // Output: (15.0 / 1_000_000) * 500 = 0.0075 22 | // Total: 0.003 + 0.0075 = 0.0105 23 | cost.should.equal(0.0105); 24 | }); 25 | 26 | it("should handle missing prices", () => { 27 | const modelInfo: ModelInfo = { 28 | supportsPromptCache: true 29 | // No prices specified 30 | }; 31 | 32 | const cost = calculateApiCost(modelInfo, 1000, 500); 33 | cost.should.equal(0); 34 | }); 35 | 36 | it("should use real model configuration (Claude 3.5 Sonnet)", () => { 37 | const modelInfo: ModelInfo = { 38 | maxTokens: 8192, 39 | contextWindow: 200_000, 40 | supportsImages: true, 41 | supportsComputerUse: true, 42 | supportsPromptCache: true, 43 | inputPrice: 3.0, 44 | outputPrice: 15.0, 45 | cacheWritesPrice: 3.75, 46 | cacheReadsPrice: 0.3 47 | }; 48 | 49 | const cost = calculateApiCost(modelInfo, 2000, 1000, 1500, 500); 50 | // Cache writes: (3.75 / 1_000_000) * 1500 = 0.005625 51 | // Cache reads: (0.3 / 1_000_000) * 500 = 0.00015 52 | // Input: (3.0 / 1_000_000) * 2000 = 0.006 53 | // Output: (15.0 / 1_000_000) * 1000 = 0.015 54 | // Total: 0.005625 + 0.00015 + 0.006 + 0.015 = 0.026775 55 | cost.should.equal(0.026775); 56 | }); 57 | 58 | it("should handle zero token counts", () => { 59 | const modelInfo: ModelInfo = { 60 | supportsPromptCache: true, 61 | inputPrice: 3.0, 62 | outputPrice: 15.0, 63 | cacheWritesPrice: 3.75, 64 | cacheReadsPrice: 0.3 65 | }; 66 | 67 | const cost = calculateApiCost(modelInfo, 0, 0, 0, 0); 68 | cost.should.equal(0); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/extension/utils/cost.ts: -------------------------------------------------------------------------------- 1 | import type { ModelInfo } from "@shared/api"; 2 | 3 | 4 | export function calculateApiCost( 5 | modelInfo: ModelInfo, 6 | inputTokens: number, 7 | outputTokens: number, 8 | cacheCreationInputTokens?: number, 9 | cacheReadInputTokens?: number 10 | ): number { 11 | const modelCacheWritesPrice = modelInfo.cacheWritesPrice; 12 | let cacheWritesCost = 0; 13 | if (cacheCreationInputTokens && modelCacheWritesPrice) { 14 | cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens; 15 | } 16 | const modelCacheReadsPrice = modelInfo.cacheReadsPrice; 17 | let cacheReadsCost = 0; 18 | if (cacheReadInputTokens && modelCacheReadsPrice) { 19 | cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens; 20 | } 21 | const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens; 22 | const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens; 23 | const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost; 24 | return totalCost; 25 | } 26 | -------------------------------------------------------------------------------- /src/extension/utils/fs.test.ts: -------------------------------------------------------------------------------- 1 | import * as os from "node:os"; 2 | import * as path from "node:path"; 3 | import * as fs from "node:fs/promises"; 4 | 5 | import { after, describe, it } from "mocha"; 6 | 7 | import { createDirectoriesForFile, fileExistsAtPath } from "./fs"; 8 | 9 | import "should"; 10 | 11 | 12 | describe("Filesystem Utilities", () => { 13 | const tmpDir = path.join(os.tmpdir(), `recline-test-${Math.random().toString(36).slice(2)}`); 14 | 15 | // Clean up after tests 16 | after(async () => { 17 | try { 18 | await fs.rm(tmpDir, { recursive: true, force: true }); 19 | } 20 | catch { 21 | // Ignore cleanup errors 22 | } 23 | }); 24 | 25 | describe("fileExistsAtPath", () => { 26 | it("should return true for existing paths", async () => { 27 | await fs.mkdir(tmpDir, { recursive: true }); 28 | const testFile = path.join(tmpDir, "test.txt"); 29 | await fs.writeFile(testFile, "test"); 30 | 31 | const exists = await fileExistsAtPath(testFile); 32 | exists.should.be.true(); 33 | }); 34 | 35 | it("should return false for non-existing paths", async () => { 36 | const nonExistentPath = path.join(tmpDir, "does-not-exist.txt"); 37 | const exists = await fileExistsAtPath(nonExistentPath); 38 | exists.should.be.false(); 39 | }); 40 | }); 41 | 42 | describe("createDirectoriesForFile", () => { 43 | it("should create all necessary directories", async () => { 44 | const deepPath = path.join(tmpDir, "deep", "nested", "dir", "file.txt"); 45 | const createdDirs = await createDirectoriesForFile(deepPath); 46 | 47 | // Verify directories were created 48 | createdDirs.length.should.be.greaterThan(0); 49 | for (const dir of createdDirs) { 50 | const exists = await fileExistsAtPath(dir); 51 | exists.should.be.true(); 52 | } 53 | }); 54 | 55 | it("should handle existing directories", async () => { 56 | const existingDir = path.join(tmpDir, "existing"); 57 | await fs.mkdir(existingDir, { recursive: true }); 58 | 59 | const filePath = path.join(existingDir, "file.txt"); 60 | const createdDirs = await createDirectoriesForFile(filePath); 61 | 62 | // Should not create any new directories 63 | createdDirs.length.should.equal(0); 64 | }); 65 | 66 | it("should normalize paths", async () => { 67 | const unnormalizedPath = path.join(tmpDir, "a", "..", "b", ".", "file.txt"); 68 | const createdDirs = await createDirectoriesForFile(unnormalizedPath); 69 | 70 | // Should create only the necessary directory 71 | createdDirs.length.should.equal(1); 72 | const exists = await fileExistsAtPath(path.join(tmpDir, "b")); 73 | exists.should.be.true(); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/extension/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import * as path from "node:path"; 3 | 4 | 5 | /** 6 | * Asynchronously creates all non-existing subdirectories for a given file path 7 | * and collects them in an array for later deletion. 8 | * 9 | * @param filePath - The full path to a file. 10 | * @returns A promise that resolves to an array of newly created directories. 11 | */ 12 | export async function createDirectoriesForFile(filePath: string): Promise { 13 | const newDirectories: string[] = []; 14 | const normalizedFilePath = path.normalize(filePath); // Normalize path for cross-platform compatibility 15 | const directoryPath = path.dirname(normalizedFilePath); 16 | 17 | let currentPath = directoryPath; 18 | const dirsToCreate: string[] = []; 19 | 20 | // Traverse up the directory tree and collect missing directories 21 | while (!(await fileExistsAtPath(currentPath))) { 22 | dirsToCreate.push(currentPath); 23 | currentPath = path.dirname(currentPath); 24 | } 25 | 26 | // Create directories from the topmost missing one down to the target directory 27 | for (let i = dirsToCreate.length - 1; i >= 0; i--) { 28 | await fs.mkdir(dirsToCreate[i]); 29 | newDirectories.push(dirsToCreate[i]); 30 | } 31 | 32 | return newDirectories; 33 | } 34 | 35 | /** 36 | * Helper function to check if a path exists. 37 | * 38 | * @param path - The path to check. 39 | * @returns A promise that resolves to true if the path exists, false otherwise. 40 | */ 41 | export async function fileExistsAtPath(filePath: string): Promise { 42 | try { 43 | await fs.access(filePath); 44 | return true; 45 | } 46 | catch { 47 | return false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/extension/utils/path.test.ts: -------------------------------------------------------------------------------- 1 | import * as os from "node:os"; 2 | import * as path from "node:path"; 3 | 4 | import { describe, it } from "mocha"; 5 | 6 | import { arePathsEqual, getReadablePath } from "./path"; 7 | 8 | import "should"; 9 | 10 | 11 | describe("Path Utilities", () => { 12 | describe("arePathsEqual", () => { 13 | it("should handle undefined paths", () => { 14 | arePathsEqual(undefined, undefined).should.be.true(); 15 | arePathsEqual("foo", undefined).should.be.false(); 16 | arePathsEqual(undefined, "foo").should.be.false(); 17 | }); 18 | 19 | it("should handle case sensitivity based on platform", () => { 20 | if (process.platform === "win32") { 21 | arePathsEqual("FOO/BAR", "foo/bar").should.be.true(); 22 | } 23 | else { 24 | arePathsEqual("FOO/BAR", "foo/bar").should.be.false(); 25 | } 26 | }); 27 | 28 | it("should handle normalized paths", () => { 29 | arePathsEqual("/tmp/./dir", "/tmp/../tmp/dir").should.be.true(); 30 | arePathsEqual("/tmp/./dir", "/tmp/../dir").should.be.false(); 31 | }); 32 | }); 33 | 34 | describe("getReadablePath", () => { 35 | it("should handle desktop path", () => { 36 | const desktop = path.join(os.homedir(), "Desktop"); 37 | const testPath = path.join(desktop, "test.txt"); 38 | getReadablePath(desktop, "test.txt").should.equal(testPath.replace(/\\/g, "/")); 39 | }); 40 | 41 | it("should show relative paths within cwd", () => { 42 | const cwd = "/home/user/project"; 43 | const filePath = "/home/user/project/src/file.txt"; 44 | getReadablePath(cwd, filePath).should.equal("src/file.txt"); 45 | }); 46 | 47 | it("should show basename when path equals cwd", () => { 48 | const cwd = "/home/user/project"; 49 | getReadablePath(cwd, cwd).should.equal("project"); 50 | }); 51 | 52 | it("should show absolute path when outside cwd", () => { 53 | const cwd = "/home/user/project"; 54 | const filePath = "/home/user/other/file.txt"; 55 | getReadablePath(cwd, filePath).should.equal(filePath); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/extension/utils/sanitize.ts: -------------------------------------------------------------------------------- 1 | import validator from "validator"; 2 | import stripAnsi from "strip-ansi"; 3 | 4 | 5 | // Unicode-aware regex patterns for handling terminal and code syntax 6 | const SHELL_PROMPT = /[%$#>]\s*$/; 7 | // eslint-disable-next-line no-control-regex 8 | const CONTROL_CHARS = /[\x00-\x08\v\f\x0E-\x1F]/g; // Control chars except tab, newline, and carriage return 9 | 10 | export interface SanitizeOptions { 11 | /** Whether to preserve newlines. Defaults to true. */ 12 | preserveNewlines?: boolean; 13 | /** Whether to strip ANSI escape sequences. Defaults to true. */ 14 | stripAnsi?: boolean; 15 | /** Whether to remove shell prompts. Defaults to false. */ 16 | removePrompts?: boolean; 17 | } 18 | 19 | /** 20 | * Core sanitization function that handles various text cleaning needs 21 | * while properly preserving valid characters including Unicode. 22 | * Uses validator.js for robust character handling and stripAnsi for terminal output. 23 | */ 24 | function sanitizeText(text: string, options: SanitizeOptions = {}): string { 25 | const { 26 | preserveNewlines = true, 27 | stripAnsi: shouldStripAnsi = true, 28 | removePrompts = false 29 | } = options; 30 | 31 | if (typeof text !== "string") { 32 | return ""; 33 | } 34 | 35 | let result = text; 36 | 37 | // Step 1: Handle ANSI escape sequences 38 | if (shouldStripAnsi) { 39 | result = stripAnsi(result); 40 | } 41 | 42 | // Step 2: Normalize line endings and handle control characters 43 | result = result 44 | .replace(/\r\n/g, "\n") 45 | .replace(/\r/g, "\n") 46 | .replace(CONTROL_CHARS, ""); 47 | 48 | // Step 3: Strip low ASCII while preserving newlines if requested 49 | try { 50 | // validator.stripLow may throw on invalid UTF-8 51 | result = validator.stripLow(result, preserveNewlines); 52 | } 53 | catch (err) { 54 | // Fallback to basic control char removal if validator fails 55 | console.warn("validator.stripLow failed, using fallback", err); 56 | if (!preserveNewlines) { 57 | result = result.replace(/\n/g, ""); 58 | } 59 | } 60 | 61 | // Step 4: Handle shell prompts if requested 62 | if (removePrompts) { 63 | result = result.replace(SHELL_PROMPT, ""); 64 | } 65 | 66 | return result.trim(); 67 | } 68 | 69 | /** 70 | * Sanitizes user input while properly preserving Unicode characters and newlines 71 | */ 72 | export function sanitizeUserInput(text: string): string { 73 | return sanitizeText(text, { 74 | preserveNewlines: true, 75 | stripAnsi: true, 76 | removePrompts: false 77 | }); 78 | } 79 | 80 | /** 81 | * Sanitizes terminal output with special handling for shell artifacts 82 | */ 83 | export function sanitizeTerminalOutput(text: string): string { 84 | return sanitizeText(text, { 85 | preserveNewlines: true, 86 | stripAnsi: true, 87 | removePrompts: true 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/extension/utils/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fixes incorrectly escaped HTML entities in AI model outputs 3 | * @param text String potentially containing incorrectly escaped HTML entities from AI models 4 | * @returns String with HTML entities converted back to normal characters 5 | */ 6 | export function fixModelHtmlEscaping(text) { 7 | return text 8 | .replace(/>/g, ">") 9 | .replace(/</g, "<") 10 | .replace(/"/g, "\"") 11 | .replace(/&/g, "&") 12 | .replace(/'/g, "'"); 13 | } 14 | 15 | /** 16 | * Removes invalid characters (like the replacement character �) from a string 17 | * @param text String potentially containing invalid characters 18 | * @returns String with invalid characters removed 19 | */ 20 | export function removeInvalidChars(text: string) { 21 | return text.replace(/\uFFFD/g, ""); 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/AutoApprovalSettings.ts: -------------------------------------------------------------------------------- 1 | export interface AutoApprovalSettings { 2 | // Whether auto-approval is enabled 3 | enabled: boolean; 4 | // Individual action permissions 5 | actions: { 6 | readFiles: boolean; // Read files and directories 7 | editFiles: boolean; // Edit files 8 | executeCommands: boolean; // Execute safe commands 9 | useBrowser: boolean; // Use browser 10 | useMcp: boolean; // Use MCP servers 11 | }; 12 | // Global settings 13 | maxRequests: number; // Maximum number of auto-approved requests 14 | enableNotifications: boolean; // Show notifications for approval and task completion 15 | } 16 | 17 | export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = { 18 | enabled: false, 19 | actions: { 20 | readFiles: false, 21 | editFiles: false, 22 | executeCommands: false, 23 | useBrowser: false, 24 | useMcp: false 25 | }, 26 | maxRequests: 20, 27 | enableNotifications: false 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/ExtensionMessage.ts: -------------------------------------------------------------------------------- 1 | // type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello' 2 | 3 | import type * as vscode from "vscode"; 4 | 5 | import type { McpServer } from "./mcp"; 6 | import type { HistoryItem } from "./HistoryItem"; 7 | import type { ApiConfiguration, ModelInfo } from "./api"; 8 | import type { AutoApprovalSettings } from "./AutoApprovalSettings"; 9 | 10 | 11 | // webview will hold state 12 | export interface ExtensionMessage { 13 | type: 14 | | "action" 15 | | "state" 16 | | "selectedImages" 17 | | "ollamaModels" 18 | | "lmStudioModels" 19 | | "theme" 20 | | "workspaceUpdated" 21 | | "invoke" 22 | | "partialMessage" 23 | | "openRouterModels" 24 | | "mcpServers" 25 | | "vsCodeLmSelectors"; 26 | text?: string; 27 | action?: 28 | | "chatButtonClicked" 29 | | "mcpButtonClicked" 30 | | "settingsButtonClicked" 31 | | "historyButtonClicked" 32 | | "didBecomeVisible"; 33 | invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"; 34 | state?: ExtensionState; 35 | images?: string[]; 36 | ollamaModels?: string[]; 37 | lmStudioModels?: string[]; 38 | filePaths?: string[]; 39 | partialMessage?: ReclineMessage; 40 | openRouterModels?: Record; 41 | mcpServers?: McpServer[]; 42 | vsCodeLmSelectors?: vscode.LanguageModelChatSelector[]; 43 | } 44 | 45 | export interface ExtensionState { 46 | version: string; 47 | apiConfiguration?: ApiConfiguration; 48 | customInstructions?: string; 49 | uriScheme?: string; 50 | reclineMessages: ReclineMessage[]; 51 | taskHistory: HistoryItem[]; 52 | shouldShowAnnouncement: boolean; 53 | autoApprovalSettings: AutoApprovalSettings; 54 | } 55 | 56 | export interface ReclineMessage { 57 | ts: number; 58 | type: "ask" | "say"; 59 | ask?: ReclineAsk; 60 | say?: ReclineSay; 61 | text?: string; 62 | images?: string[]; 63 | partial?: boolean; 64 | } 65 | 66 | export type ReclineAsk = 67 | | "followup" 68 | | "command" 69 | | "command_output" 70 | | "completion_result" 71 | | "tool" 72 | | "api_req_failed" 73 | | "resume_task" 74 | | "resume_completed_task" 75 | | "mistake_limit_reached" 76 | | "auto_approval_max_req_reached" 77 | | "browser_action_launch" 78 | | "use_mcp_server"; 79 | 80 | export type ReclineSay = 81 | | "task" 82 | | "error" 83 | | "api_req_started" 84 | | "api_req_finished" 85 | | "text" 86 | | "completion_result" 87 | | "user_feedback" 88 | | "user_feedback_diff" 89 | | "api_req_retried" 90 | | "command" 91 | | "command_output" 92 | | "tool" 93 | | "shell_integration_warning" 94 | | "browser_action_launch" 95 | | "browser_action" 96 | | "browser_action_result" 97 | | "mcp_server_request_started" 98 | | "mcp_server_response" 99 | | "use_mcp_server" 100 | | "diff_error"; 101 | 102 | export interface ReclineSayTool { 103 | tool: 104 | | "editedExistingFile" 105 | | "newFileCreated" 106 | | "readFile" 107 | | "listFilesTopLevel" 108 | | "listFilesRecursive" 109 | | "listCodeDefinitionNames" 110 | | "searchFiles"; 111 | path?: string; 112 | diff?: string; 113 | content?: string; 114 | regex?: string; 115 | filePattern?: string; 116 | } 117 | 118 | // must keep in sync with system prompt 119 | export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const; 120 | export type BrowserAction = (typeof browserActions)[number]; 121 | 122 | export interface ReclineSayBrowserAction { 123 | action: BrowserAction; 124 | coordinate?: string; 125 | text?: string; 126 | } 127 | 128 | export interface BrowserActionResult { 129 | screenshot?: string; 130 | logs?: string; 131 | currentUrl?: string; 132 | currentMousePosition?: string; 133 | } 134 | 135 | export interface ReclineAskUseMcpServer { 136 | serverName: string; 137 | type: "use_mcp_tool" | "access_mcp_resource"; 138 | toolName?: string; 139 | arguments?: string; 140 | uri?: string; 141 | } 142 | 143 | export interface ReclineApiReqInfo { 144 | request?: string; 145 | tokensIn?: number; 146 | tokensOut?: number; 147 | cacheWrites?: number; 148 | cacheReads?: number; 149 | cost?: number; 150 | cancelReason?: ReclineApiReqCancelReason; 151 | streamingFailedMessage?: string; 152 | } 153 | 154 | export type ReclineApiReqCancelReason = "streaming_failed" | "user_cancelled"; 155 | -------------------------------------------------------------------------------- /src/shared/HistoryItem.ts: -------------------------------------------------------------------------------- 1 | export interface HistoryItem { 2 | id: string; 3 | ts: number; 4 | task: string; 5 | tokensIn: number; 6 | tokensOut: number; 7 | cacheWrites?: number; 8 | cacheReads?: number; 9 | totalCost: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/WebviewMessage.ts: -------------------------------------------------------------------------------- 1 | import type { ApiConfiguration } from "./api"; 2 | import type { AutoApprovalSettings } from "./AutoApprovalSettings"; 3 | 4 | 5 | export interface WebviewMessage { 6 | type: 7 | | "apiConfiguration" 8 | | "customInstructions" 9 | | "webviewDidLaunch" 10 | | "newTask" 11 | | "askResponse" 12 | | "clearTask" 13 | | "didShowAnnouncement" 14 | | "selectImages" 15 | | "exportCurrentTask" 16 | | "showTaskWithId" 17 | | "deleteTaskWithId" 18 | | "exportTaskWithId" 19 | | "resetState" 20 | | "requestOllamaModels" 21 | | "requestLmStudioModels" 22 | | "requestVsCodeLmSelectors" 23 | | "openImage" 24 | | "openFile" 25 | | "openMention" 26 | | "cancelTask" 27 | | "refreshOpenRouterModels" 28 | | "openMcpSettings" 29 | | "restartMcpServer" 30 | | "autoApprovalSettings"; 31 | text?: string; 32 | askResponse?: ReclineAskResponse; 33 | apiConfiguration?: ApiConfiguration; 34 | images?: string[]; 35 | bool?: boolean; 36 | autoApprovalSettings?: AutoApprovalSettings; 37 | } 38 | 39 | export type ReclineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"; 40 | -------------------------------------------------------------------------------- /src/shared/array.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "mocha"; 2 | 3 | import { findLast, findLastIndex } from "./array"; 4 | 5 | import "should"; 6 | 7 | 8 | describe("Array Utilities", () => { 9 | describe("findLastIndex", () => { 10 | it("should find last matching element's index", () => { 11 | const array = [1, 2, 3, 2, 1]; 12 | const index = findLastIndex(array, x => x === 2); 13 | index.should.equal(3); // last '2' is at index 3 14 | }); 15 | 16 | it("should return -1 when no element matches", () => { 17 | const array = [1, 2, 3]; 18 | const index = findLastIndex(array, x => x === 4); 19 | index.should.equal(-1); 20 | }); 21 | 22 | it("should handle empty arrays", () => { 23 | const array: number[] = []; 24 | const index = findLastIndex(array, x => x === 1); 25 | index.should.equal(-1); 26 | }); 27 | 28 | it("should work with different types", () => { 29 | const array = ["a", "b", "c", "b", "a"]; 30 | const index = findLastIndex(array, x => x === "b"); 31 | index.should.equal(3); 32 | }); 33 | 34 | it("should provide correct index in predicate", () => { 35 | const array = [1, 2, 3]; 36 | const indices: number[] = []; 37 | findLastIndex(array, (_, index) => { 38 | indices.push(index); 39 | return false; 40 | }); 41 | indices.should.deepEqual([2, 1, 0]); // Should iterate in reverse 42 | }); 43 | 44 | it("should provide array reference in predicate", () => { 45 | const array = [1, 2, 3]; 46 | findLastIndex(array, (_, __, arr) => { 47 | arr.should.equal(array); // Should pass original array 48 | return false; 49 | }); 50 | }); 51 | }); 52 | 53 | describe("findLast", () => { 54 | it("should find last matching element", () => { 55 | const array = [1, 2, 3, 2, 1]; 56 | const element = findLast(array, x => x === 2); 57 | should(element).not.be.undefined(); 58 | element!.should.equal(2); 59 | }); 60 | 61 | it("should return undefined when no element matches", () => { 62 | const array = [1, 2, 3]; 63 | const element = findLast(array, x => x === 4); 64 | should(element).be.undefined(); 65 | }); 66 | 67 | it("should handle empty arrays", () => { 68 | const array: number[] = []; 69 | const element = findLast(array, x => x === 1); 70 | should(element).be.undefined(); 71 | }); 72 | 73 | it("should work with object arrays", () => { 74 | const array = [ 75 | { id: 1, value: "a" }, 76 | { id: 2, value: "b" }, 77 | { id: 3, value: "a" } 78 | ]; 79 | const element = findLast(array, x => x.value === "a"); 80 | should(element).not.be.undefined(); 81 | element!.should.deepEqual({ id: 3, value: "a" }); 82 | }); 83 | 84 | it("should provide correct index in predicate", () => { 85 | const array = [1, 2, 3]; 86 | const indices: number[] = []; 87 | findLast(array, (_, index) => { 88 | indices.push(index); 89 | return false; 90 | }); 91 | indices.should.deepEqual([2, 1, 0]); // Should iterate in reverse 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/shared/array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the index of the last element in the array where predicate is true, and -1 3 | * otherwise. 4 | * @param array The source array to search in 5 | * @param predicate find calls predicate once for each element of the array, in descending 6 | * order, until it finds one where predicate returns true. If such an element is found, 7 | * findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1. 8 | */ 9 | export function findLastIndex(array: Array, predicate: (value: T, index: number, obj: T[]) => boolean): number { 10 | let l = array.length; 11 | while (l--) { 12 | if (predicate(array[l], l, array)) { 13 | return l; 14 | } 15 | } 16 | return -1; 17 | } 18 | 19 | export function findLast(array: Array, predicate: (value: T, index: number, obj: T[]) => boolean): T | undefined { 20 | const index = findLastIndex(array, predicate); 21 | return index === -1 ? undefined : array[index]; 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/combineApiRequests.ts: -------------------------------------------------------------------------------- 1 | import type { ReclineMessage } from "./ExtensionMessage"; 2 | 3 | 4 | /** 5 | * Combines API request start and finish messages in an array of ReclineMessages. 6 | * 7 | * This function looks for pairs of 'api_req_started' and 'api_req_finished' messages. 8 | * When it finds a pair, it combines them into a single 'api_req_combined' message. 9 | * The JSON data in the text fields of both messages are merged. 10 | * 11 | * @param messages - An array of ReclineMessage objects to process. 12 | * @returns A new array of ReclineMessage objects with API requests combined. 13 | * 14 | * @example 15 | * const messages = [ 16 | * { type: "say", say: "api_req_started", text: '{"request":"GET /api/data"}', ts: 1000 }, 17 | * { type: "say", say: "api_req_finished", text: '{"cost":0.005}', ts: 1001 } 18 | * ]; 19 | * const result = combineApiRequests(messages); 20 | * // Result: [{ type: "say", say: "api_req_started", text: '{"request":"GET /api/data","cost":0.005}', ts: 1000 }] 21 | */ 22 | export function combineApiRequests(messages: ReclineMessage[]): ReclineMessage[] { 23 | const combinedApiRequests: ReclineMessage[] = []; 24 | 25 | for (let i = 0; i < messages.length; i++) { 26 | if (messages[i].type === "say" && messages[i].say === "api_req_started") { 27 | const startedRequest = JSON.parse(messages[i].text || "{}"); 28 | let j = i + 1; 29 | 30 | while (j < messages.length) { 31 | if (messages[j].type === "say" && messages[j].say === "api_req_finished") { 32 | const finishedRequest = JSON.parse(messages[j].text || "{}"); 33 | const combinedRequest = { ...startedRequest, ...finishedRequest }; 34 | 35 | combinedApiRequests.push({ 36 | ...messages[i], 37 | text: JSON.stringify(combinedRequest) 38 | }); 39 | 40 | i = j; // Skip to the api_req_finished message 41 | break; 42 | } 43 | j++; 44 | } 45 | 46 | if (j === messages.length) { 47 | // If no matching api_req_finished found, keep the original api_req_started 48 | combinedApiRequests.push(messages[i]); 49 | } 50 | } 51 | } 52 | 53 | // Replace original api_req_started and remove api_req_finished 54 | return messages 55 | .filter(msg => !(msg.type === "say" && msg.say === "api_req_finished")) 56 | .map((msg) => { 57 | if (msg.type === "say" && msg.say === "api_req_started") { 58 | const combinedRequest = combinedApiRequests.find(req => req.ts === msg.ts); 59 | return combinedRequest || msg; 60 | } 61 | return msg; 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/shared/combineCommandSequences.ts: -------------------------------------------------------------------------------- 1 | import type { ReclineMessage } from "./ExtensionMessage"; 2 | 3 | 4 | /** 5 | * Combines sequences of command and command_output messages in an array of ReclineMessages. 6 | * 7 | * This function processes an array of ReclineMessages objects, looking for sequences 8 | * where a 'command' message is followed by one or more 'command_output' messages. 9 | * When such a sequence is found, it combines them into a single message, merging 10 | * their text contents. 11 | * 12 | * @param messages - An array of ReclineMessage objects to process. 13 | * @returns A new array of ReclineMessage objects with command sequences combined. 14 | * 15 | * @example 16 | * const messages: ReclineMessage[] = [ 17 | * { type: 'ask', ask: 'command', text: 'ls', ts: 1625097600000 }, 18 | * { type: 'ask', ask: 'command_output', text: 'file1.txt', ts: 1625097601000 }, 19 | * { type: 'ask', ask: 'command_output', text: 'file2.txt', ts: 1625097602000 } 20 | * ]; 21 | * const result = simpleCombineCommandSequences(messages); 22 | * // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }] 23 | */ 24 | export function combineCommandSequences(messages: ReclineMessage[]): ReclineMessage[] { 25 | const combinedCommands: ReclineMessage[] = []; 26 | 27 | // First pass: combine commands with their outputs 28 | for (let i = 0; i < messages.length; i++) { 29 | if (messages[i].type === "ask" && (messages[i].ask === "command" || messages[i].say === "command")) { 30 | let combinedText = messages[i].text || ""; 31 | let didAddOutput = false; 32 | let j = i + 1; 33 | 34 | while (j < messages.length) { 35 | if (messages[j].type === "ask" && (messages[j].ask === "command" || messages[j].say === "command")) { 36 | // Stop if we encounter the next command 37 | break; 38 | } 39 | if (messages[j].ask === "command_output" || messages[j].say === "command_output") { 40 | if (!didAddOutput) { 41 | // Add a newline before the first output 42 | combinedText += `\n${COMMAND_OUTPUT_STRING}`; 43 | didAddOutput = true; 44 | } 45 | // handle cases where we receive empty command_output (ie when extension is relinquishing control over exit command button) 46 | const output = messages[j].text || ""; 47 | if (output.length > 0) { 48 | combinedText += `\n${output}`; 49 | } 50 | } 51 | j++; 52 | } 53 | 54 | combinedCommands.push({ 55 | ...messages[i], 56 | text: combinedText 57 | }); 58 | 59 | i = j - 1; // Move to the index just before the next command or end of array 60 | } 61 | } 62 | 63 | // Second pass: remove command_outputs and replace original commands with combined ones 64 | return messages 65 | .filter(msg => !(msg.ask === "command_output" || msg.say === "command_output")) 66 | .map((msg) => { 67 | if (msg.type === "ask" && (msg.ask === "command" || msg.say === "command")) { 68 | const combinedCommand = combinedCommands.find(cmd => cmd.ts === msg.ts); 69 | return combinedCommand || msg; 70 | } 71 | return msg; 72 | }); 73 | } 74 | export const COMMAND_OUTPUT_STRING = "Output:"; 75 | export const COMMAND_REQ_APP_STRING = "REQ_APP"; 76 | -------------------------------------------------------------------------------- /src/shared/context-mentions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Mention regex: 3 | - **Purpose**: 4 | - To identify and highlight specific mentions in text that start with '@'. 5 | - These mentions can be file paths, URLs, or the exact word 'problems'. 6 | - Ensures that trailing punctuation marks (like commas, periods, etc.) are not included in the match, allowing punctuation to follow the mention without being part of it. 7 | 8 | - **Regex Breakdown**: 9 | - `/@`: 10 | - **@**: The mention must start with the '@' symbol. 11 | 12 | - `((?:\/|\w+:\/\/)[^\s]+?|problems\b)`: 13 | - **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns. 14 | - `(?:\/|\w+:\/\/)`: 15 | - **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing. 16 | - `\/`: 17 | - **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'. 18 | - `|`: Logical OR. 19 | - `\w+:\/\/`: 20 | - **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc. 21 | - `[^\s]+?`: 22 | - **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace. 23 | - **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation. 24 | - `|`: Logical OR. 25 | - `problems\b`: 26 | - **Exact Word ('problems')**: Matches the exact word 'problems'. 27 | - **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic'). 28 | 29 | - `(?=[.,;:!?]?(?=[\s\r\n]|$))`: 30 | - **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match. 31 | - `[.,;:!?]?`: 32 | - **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks. 33 | - `(?=[\s\r\n]|$)`: 34 | - **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string. 35 | 36 | - **Summary**: 37 | - The regex effectively matches: 38 | - Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path). 39 | - URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters). 40 | - The exact word 'problems'. 41 | - It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text. 42 | 43 | - **Global Regex**: 44 | - `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string. 45 | 46 | */ 47 | export const mentionRegex = /@((?:\/|\w+:\/\/)\S+?|problems\b)(?=[.,;:!?]?(?:\s|$))/; 48 | export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g"); 49 | -------------------------------------------------------------------------------- /src/shared/getApiMetrics.ts: -------------------------------------------------------------------------------- 1 | import type { ReclineMessage } from "./ExtensionMessage"; 2 | 3 | 4 | interface ApiMetrics { 5 | totalTokensIn: number; 6 | totalTokensOut: number; 7 | totalCacheWrites?: number; 8 | totalCacheReads?: number; 9 | totalCost: number; 10 | } 11 | 12 | /** 13 | * Calculates API metrics from an array of ReclineMessages. 14 | * 15 | * This function processes 'api_req_started' messages that have been combined with their 16 | * corresponding 'api_req_finished' messages by the combineApiRequests function. 17 | * It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages. 18 | * 19 | * @param messages - An array of ReclineMessage objects to process. 20 | * @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, and totalCost. 21 | * 22 | * @example 23 | * const messages = [ 24 | * { type: "say", say: "api_req_started", text: '{"request":"GET /api/data","tokensIn":10,"tokensOut":20,"cost":0.005}', ts: 1000 } 25 | * ]; 26 | * const { totalTokensIn, totalTokensOut, totalCost } = getApiMetrics(messages); 27 | * // Result: { totalTokensIn: 10, totalTokensOut: 20, totalCost: 0.005 } 28 | */ 29 | export function getApiMetrics(messages: ReclineMessage[]): ApiMetrics { 30 | const result: ApiMetrics = { 31 | totalTokensIn: 0, 32 | totalTokensOut: 0, 33 | totalCacheWrites: undefined, 34 | totalCacheReads: undefined, 35 | totalCost: 0 36 | }; 37 | 38 | messages.forEach((message) => { 39 | if (message.type === "say" && message.say === "api_req_started" && message.text) { 40 | try { 41 | const parsedData = JSON.parse(message.text); 42 | const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedData; 43 | 44 | if (typeof tokensIn === "number") { 45 | result.totalTokensIn += tokensIn; 46 | } 47 | if (typeof tokensOut === "number") { 48 | result.totalTokensOut += tokensOut; 49 | } 50 | if (typeof cacheWrites === "number") { 51 | result.totalCacheWrites = (result.totalCacheWrites ?? 0) + cacheWrites; 52 | } 53 | if (typeof cacheReads === "number") { 54 | result.totalCacheReads = (result.totalCacheReads ?? 0) + cacheReads; 55 | } 56 | if (typeof cost === "number") { 57 | result.totalCost += cost; 58 | } 59 | } 60 | catch (error) { 61 | console.error("Error parsing JSON:", error); 62 | } 63 | } 64 | }); 65 | 66 | return result; 67 | } 68 | -------------------------------------------------------------------------------- /src/shared/mcp.ts: -------------------------------------------------------------------------------- 1 | export interface McpServer { 2 | name: string; 3 | config: string; 4 | status: "connected" | "connecting" | "disconnected"; 5 | error?: string; 6 | tools?: McpTool[]; 7 | resources?: McpResource[]; 8 | resourceTemplates?: McpResourceTemplate[]; 9 | } 10 | 11 | export interface McpTool { 12 | name: string; 13 | description?: string; 14 | inputSchema?: object; 15 | } 16 | 17 | export interface McpResource { 18 | uri: string; 19 | name: string; 20 | mimeType?: string; 21 | description?: string; 22 | } 23 | 24 | export interface McpResourceTemplate { 25 | uriTemplate: string; 26 | name: string; 27 | description?: string; 28 | mimeType?: string; 29 | } 30 | 31 | export interface McpResourceResponse { 32 | _meta?: Record; 33 | contents: Array<{ 34 | uri: string; 35 | mimeType?: string; 36 | text?: string; 37 | blob?: string; 38 | }>; 39 | } 40 | 41 | export interface McpToolCallResponse { 42 | _meta?: Record; 43 | content: Array< 44 | | { 45 | type: "text"; 46 | text: string; 47 | } 48 | | { 49 | type: "image"; 50 | data: string; 51 | mimeType: string; 52 | } 53 | | { 54 | type: "resource"; 55 | resource: { 56 | uri: string; 57 | mimeType?: string; 58 | text?: string; 59 | blob?: string; 60 | }; 61 | } 62 | >; 63 | isError?: boolean; 64 | } 65 | -------------------------------------------------------------------------------- /src/shared/vsCodeSelectorUtils.ts: -------------------------------------------------------------------------------- 1 | import type * as vscode from "vscode"; 2 | 3 | import { isEqual } from "es-toolkit"; 4 | 5 | 6 | export const SELECTOR_SEPARATOR: string = " / "; 7 | 8 | 9 | export function isVsCodeLmModelSelectorEqual(a: any, b: any): boolean { 10 | return isEqual(a, b); 11 | } 12 | 13 | export function stringifyVsCodeLmModelSelector(selector: vscode.LanguageModelChatSelector): string { 14 | if (!selector.vendor || !selector.family) { 15 | return selector.id || ""; 16 | } 17 | 18 | return `${selector.vendor}${SELECTOR_SEPARATOR}${selector.family}`; 19 | } 20 | 21 | export function parseVsCodeLmModelSelector(stringifiedSelector: string): vscode.LanguageModelChatSelector { 22 | if (!stringifiedSelector.includes(SELECTOR_SEPARATOR)) { 23 | return { id: stringifiedSelector }; 24 | } 25 | 26 | const parts: string[] = stringifiedSelector.split(SELECTOR_SEPARATOR); 27 | if (parts.length !== 2) { 28 | return { id: stringifiedSelector }; 29 | } 30 | 31 | return { vendor: parts[0], family: parts[1] }; 32 | } 33 | -------------------------------------------------------------------------------- /src/webview-ui/App.tsx: -------------------------------------------------------------------------------- 1 | import type { ExtensionMessage } from "../../src/shared/ExtensionMessage"; 2 | 3 | import { useEvent } from "react-use"; 4 | import { useCallback, useEffect, useState } from "react"; 5 | 6 | import McpView from "./components/mcp/McpView"; 7 | import ChatView from "./components/chat/ChatView"; 8 | import { vscodeApiWrapper } from "./utils/vscode"; 9 | import HistoryView from "./components/history/HistoryView"; 10 | import WelcomeView from "./components/welcome/WelcomeView"; 11 | import SettingsView from "./components/settings/SettingsView"; 12 | import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"; 13 | 14 | 15 | function AppContent() { 16 | const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState(); 17 | const [showSettings, setShowSettings] = useState(false); 18 | const [showHistory, setShowHistory] = useState(false); 19 | const [showMcp, setShowMcp] = useState(false); 20 | const [showAnnouncement, setShowAnnouncement] = useState(false); 21 | 22 | const handleMessage = useCallback((e: MessageEvent) => { 23 | const message: ExtensionMessage = e.data; 24 | switch (message.type) { 25 | case "action": 26 | switch (message.action!) { 27 | case "settingsButtonClicked": 28 | setShowSettings(true); 29 | setShowHistory(false); 30 | setShowMcp(false); 31 | break; 32 | case "historyButtonClicked": 33 | setShowSettings(false); 34 | setShowHistory(true); 35 | setShowMcp(false); 36 | break; 37 | case "mcpButtonClicked": 38 | setShowSettings(false); 39 | setShowHistory(false); 40 | setShowMcp(true); 41 | break; 42 | case "chatButtonClicked": 43 | setShowSettings(false); 44 | setShowHistory(false); 45 | setShowMcp(false); 46 | break; 47 | } 48 | break; 49 | } 50 | }, []); 51 | 52 | useEvent("message", handleMessage); 53 | 54 | useEffect(() => { 55 | if (shouldShowAnnouncement) { 56 | setShowAnnouncement(true); 57 | vscodeApiWrapper.postMessage({ type: "didShowAnnouncement" }); 58 | } 59 | }, [shouldShowAnnouncement]); 60 | 61 | if (!didHydrateState) { 62 | return null; 63 | } 64 | 65 | return ( 66 | <> 67 | {showWelcome ? ( 68 | 69 | ) : ( 70 | <> 71 | {showSettings && setShowSettings(false)} />} 72 | {showHistory && setShowHistory(false)} />} 73 | {showMcp && setShowMcp(false)} />} 74 | {/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */} 75 | { 77 | setShowSettings(false); 78 | setShowMcp(false); 79 | setShowHistory(true); 80 | }} 81 | isHidden={showSettings || showHistory || showMcp} 82 | showAnnouncement={showAnnouncement} 83 | hideAnnouncement={() => { 84 | setShowAnnouncement(false); 85 | }} 86 | /> 87 | 88 | )} 89 | 90 | ); 91 | } 92 | 93 | function App() { 94 | return ( 95 | 96 | 97 | 98 | ); 99 | } 100 | 101 | export default App; 102 | -------------------------------------------------------------------------------- /src/webview-ui/components/common/Thumbnails.tsx: -------------------------------------------------------------------------------- 1 | import { useWindowSize } from "react-use"; 2 | import React, { memo, useLayoutEffect, useRef, useState } from "react"; 3 | 4 | import { vscodeApiWrapper } from "@webview-ui/utils/vscode"; 5 | 6 | 7 | interface ThumbnailsProps { 8 | images: string[]; 9 | style?: React.CSSProperties; 10 | setImages?: React.Dispatch>; 11 | onHeightChange?: (height: number) => void; 12 | } 13 | 14 | function Thumbnails({ images, style, setImages, onHeightChange }: ThumbnailsProps) { 15 | const [hoveredIndex, setHoveredIndex] = useState(null); 16 | const containerRef = useRef(null); 17 | const { width } = useWindowSize(); 18 | 19 | useLayoutEffect(() => { 20 | if (containerRef.current) { 21 | let height = containerRef.current.clientHeight; 22 | // some browsers return 0 for clientHeight 23 | if (!height) { 24 | height = containerRef.current.getBoundingClientRect().height; 25 | } 26 | onHeightChange?.(height); 27 | } 28 | setHoveredIndex(null); 29 | }, [images, width, onHeightChange]); 30 | 31 | const handleDelete = (index: number) => { 32 | setImages?.(prevImages => prevImages.filter((_, i) => i !== index)); 33 | }; 34 | 35 | const isDeletable = setImages !== undefined; 36 | 37 | const handleImageClick = (image: string) => { 38 | vscodeApiWrapper.postMessage({ type: "openImage", text: image }); 39 | }; 40 | 41 | return ( 42 |
52 | {images.map((image, index) => ( 53 |
setHoveredIndex(index)} 57 | onMouseLeave={() => setHoveredIndex(null)} 58 | > 59 | {`Thumbnail handleImageClick(image)} 70 | /> 71 | {isDeletable && hoveredIndex === index && ( 72 |
handleDelete(index)} 74 | style={{ 75 | position: "absolute", 76 | top: -4, 77 | right: -4, 78 | width: 13, 79 | height: 13, 80 | borderRadius: "50%", 81 | backgroundColor: "var(--vscode-badge-background)", 82 | display: "flex", 83 | justifyContent: "center", 84 | alignItems: "center", 85 | cursor: "pointer" 86 | }} 87 | > 88 | 96 | 97 |
98 | )} 99 |
100 | ))} 101 |
102 | ); 103 | } 104 | 105 | export default memo(Thumbnails); 106 | -------------------------------------------------------------------------------- /src/webview-ui/components/common/VSCodeButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; 3 | 4 | 5 | interface VSCodeButtonLinkProps { 6 | href: string; 7 | children: React.ReactNode; 8 | [key: string]: any; 9 | } 10 | 11 | const VSCodeButtonLink: React.FC = ({ href, children, ...props }) => { 12 | return ( 13 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export default VSCodeButtonLink; 26 | -------------------------------------------------------------------------------- /src/webview-ui/components/mcp/McpResourceRow.tsx: -------------------------------------------------------------------------------- 1 | import type { McpResource, McpResourceTemplate } from "@shared/mcp"; 2 | 3 | 4 | interface McpResourceRowProps { 5 | item: McpResource | McpResourceTemplate; 6 | } 7 | 8 | function McpResourceRow({ item }: McpResourceRowProps) { 9 | const hasUri = "uri" in item; 10 | const uri = hasUri ? item.uri : item.uriTemplate; 11 | 12 | return ( 13 |
19 |
26 | 27 | {uri} 28 |
29 |
36 | {item.name && item.description 37 | ? `${item.name}: ${item.description}` 38 | : !item.name && item.description 39 | ? item.description 40 | : !item.description && item.name 41 | ? item.name 42 | : "No description"} 43 |
44 |
49 | Returns 50 | 58 | {item.mimeType || "Unknown"} 59 | 60 |
61 |
62 | ); 63 | } 64 | 65 | export default McpResourceRow; 66 | -------------------------------------------------------------------------------- /src/webview-ui/components/mcp/McpToolRow.tsx: -------------------------------------------------------------------------------- 1 | import type { McpTool } from "@shared/mcp"; 2 | 3 | 4 | interface McpToolRowProps { 5 | tool: McpTool; 6 | } 7 | 8 | function McpToolRow({ tool }: McpToolRowProps) { 9 | return ( 10 |
16 |
17 | 18 | {tool.name} 19 |
20 | {tool.description && ( 21 |
29 | {tool.description} 30 |
31 | )} 32 | {tool.inputSchema 33 | && "properties" in tool.inputSchema 34 | && Object.keys(tool.inputSchema.properties as Record).length > 0 && ( 35 |
44 |
47 | Parameters 48 |
49 | {Object.entries(tool.inputSchema.properties as Record).map( 50 | ([paramName, schema]) => { 51 | const isRequired 52 | = tool.inputSchema 53 | && "required" in tool.inputSchema 54 | && Array.isArray(tool.inputSchema.required) 55 | && tool.inputSchema.required.includes(paramName); 56 | 57 | return ( 58 |
66 | 72 | {paramName} 73 | {isRequired && ( 74 | * 75 | )} 76 | 77 | 84 | {schema.description || "No description"} 85 | 86 |
87 | ); 88 | } 89 | )} 90 |
91 | )} 92 |
93 | ); 94 | } 95 | 96 | export default McpToolRow; 97 | -------------------------------------------------------------------------------- /src/webview-ui/components/welcome/WelcomeView.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; 3 | 4 | import { vscodeApiWrapper } from "@webview-ui/utils/vscode"; 5 | import ApiOptions from "@webview-ui/components/settings/ApiOptions"; 6 | import { validateApiConfiguration } from "@webview-ui/utils/validate"; 7 | import { useExtensionState } from "@webview-ui/context/ExtensionStateContext"; 8 | 9 | 10 | function WelcomeView() { 11 | const { apiConfiguration } = useExtensionState(); 12 | 13 | const [apiErrorMessage, setApiErrorMessage] = useState(undefined); 14 | 15 | const disableLetsGoButton = apiErrorMessage != null; 16 | 17 | const handleSubmit = () => { 18 | vscodeApiWrapper.postMessage({ type: "apiConfiguration", apiConfiguration }); 19 | }; 20 | 21 | useEffect(() => { 22 | setApiErrorMessage(validateApiConfiguration(apiConfiguration)); 23 | }, [apiConfiguration]); 24 | 25 | return ( 26 |
27 |

Ready When You Are.

28 |

Recline is your new AI assistant, ready to help you create, edit, and run code—all while you sit back and relax. It integrates seamlessly with your CLI and editor to make coding feel effortless.

29 | 30 | Before we dive in, let's set up your AI provider. 31 | 32 |
33 | 34 | 35 | Let's go! 36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | export default WelcomeView; 43 | -------------------------------------------------------------------------------- /src/webview-ui/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | import "./index.css"; 8 | import "@vscode/codicons/dist/codicon.css"; 9 | 10 | 11 | const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/webview-ui/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import type { ReportHandler } from "web-vitals"; 2 | 3 | 4 | function reportWebVitals(onPerfEntry?: ReportHandler): void { 5 | if (onPerfEntry && onPerfEntry instanceof Function) { 6 | import("web-vitals") 7 | .then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 8 | getCLS(onPerfEntry); 9 | getFID(onPerfEntry); 10 | getFCP(onPerfEntry); 11 | getLCP(onPerfEntry); 12 | getTTFB(onPerfEntry); 13 | }) 14 | .catch(console.error); 15 | } 16 | } 17 | 18 | export default reportWebVitals; 19 | -------------------------------------------------------------------------------- /src/webview-ui/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /src/webview-ui/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function formatLargeNumber(num: number): string { 2 | if (num >= 1e9) { 3 | return `${(num / 1e9).toFixed(1)}b`; 4 | } 5 | if (num >= 1e6) { 6 | return `${(num / 1e6).toFixed(1)}m`; 7 | } 8 | if (num >= 1e3) { 9 | return `${(num / 1e3).toFixed(1)}k`; 10 | } 11 | return num.toString(); 12 | } 13 | -------------------------------------------------------------------------------- /src/webview-ui/utils/getLanguageFromPath.ts: -------------------------------------------------------------------------------- 1 | const extensionToLanguage: { [key: string]: string } = { 2 | // Web technologies 3 | html: "html", 4 | htm: "html", 5 | css: "css", 6 | js: "javascript", 7 | jsx: "jsx", 8 | ts: "typescript", 9 | tsx: "tsx", 10 | 11 | // Backend languages 12 | py: "python", 13 | rb: "ruby", 14 | php: "php", 15 | java: "java", 16 | cs: "csharp", 17 | go: "go", 18 | rs: "rust", 19 | scala: "scala", 20 | kt: "kotlin", 21 | swift: "swift", 22 | 23 | // Markup and data 24 | json: "json", 25 | xml: "xml", 26 | yaml: "yaml", 27 | yml: "yaml", 28 | md: "markdown", 29 | csv: "csv", 30 | 31 | // Shell and scripting 32 | sh: "bash", 33 | bash: "bash", 34 | zsh: "bash", 35 | ps1: "powershell", 36 | 37 | // Configuration 38 | toml: "toml", 39 | ini: "ini", 40 | cfg: "ini", 41 | conf: "ini", 42 | 43 | // Other 44 | sql: "sql", 45 | graphql: "graphql", 46 | gql: "graphql", 47 | tex: "latex", 48 | svg: "svg", 49 | txt: "text", 50 | 51 | // C-family languages 52 | c: "c", 53 | cpp: "cpp", 54 | h: "c", 55 | hpp: "cpp", 56 | 57 | // Functional languages 58 | hs: "haskell", 59 | lhs: "haskell", 60 | elm: "elm", 61 | clj: "clojure", 62 | cljs: "clojure", 63 | erl: "erlang", 64 | ex: "elixir", 65 | exs: "elixir", 66 | 67 | // Mobile development 68 | dart: "dart", 69 | m: "objectivec", 70 | mm: "objectivec", 71 | 72 | // Game development 73 | lua: "lua", 74 | gd: "gdscript", // Godot 75 | unity: "csharp", // Unity (using C#) 76 | 77 | // Data science and ML 78 | r: "r", 79 | jl: "julia", 80 | ipynb: "jupyter" // Jupyter notebooks 81 | }; 82 | 83 | // Example usage: 84 | // console.log(getLanguageFromPath('/path/to/file.js')); // Output: javascript 85 | 86 | export function getLanguageFromPath(path: string): string | undefined { 87 | const extension = path.split(".").pop()?.toLowerCase() || ""; 88 | return extensionToLanguage[extension]; 89 | } 90 | -------------------------------------------------------------------------------- /src/webview-ui/utils/mcp.ts: -------------------------------------------------------------------------------- 1 | import type { McpResource, McpResourceTemplate } from "../../../src/shared/mcp"; 2 | 3 | 4 | /** 5 | * Matches a URI against an array of URI templates and returns the matching template 6 | * @param uri The URI to match 7 | * @param templates Array of URI templates to match against 8 | * @returns The matching template or undefined if no match is found 9 | */ 10 | export function findMatchingTemplate( 11 | uri: string, 12 | templates: McpResourceTemplate[] = [] 13 | ): McpResourceTemplate | undefined { 14 | return templates.find((template) => { 15 | // Convert template to regex pattern 16 | const pattern = String(template.uriTemplate) 17 | // First escape special regex characters 18 | .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") 19 | // Then replace {param} with ([^/]+) to match any non-slash characters 20 | // We need to use \{ and \} because we just escaped them 21 | .replace(/\\\{([^}]+)\\\}/g, "([^/]+)"); 22 | 23 | const regex = new RegExp(`^${pattern}$`); 24 | return regex.test(uri); 25 | }); 26 | } 27 | 28 | /** 29 | * Finds either an exact resource match or a matching template for a given URI 30 | * @param uri The URI to find a match for 31 | * @param resources Array of concrete resources 32 | * @param templates Array of resource templates 33 | * @returns The matching resource, template, or undefined 34 | */ 35 | export function findMatchingResourceOrTemplate( 36 | uri: string, 37 | resources: McpResource[] = [], 38 | templates: McpResourceTemplate[] = [] 39 | ): McpResource | McpResourceTemplate | undefined { 40 | // First try to find an exact resource match 41 | const exactMatch = resources.find(resource => resource.uri === uri); 42 | if (exactMatch) 43 | return exactMatch; 44 | 45 | // If no exact match, try to find a matching template 46 | return findMatchingTemplate(uri, templates); 47 | } 48 | -------------------------------------------------------------------------------- /src/webview-ui/utils/validate.ts: -------------------------------------------------------------------------------- 1 | import type { ApiConfiguration, ModelInfo } from "../../../src/shared/api"; 2 | 3 | import { openRouterDefaultModelId } from "../../../src/shared/api"; 4 | 5 | 6 | export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined { 7 | if (apiConfiguration) { 8 | switch (apiConfiguration.apiProvider) { 9 | case "anthropic": 10 | if (!apiConfiguration.apiKey) { 11 | return "You must provide a valid API key or choose a different provider."; 12 | } 13 | break; 14 | case "bedrock": 15 | if (!apiConfiguration.awsRegion) { 16 | return "You must choose a region to use with AWS Bedrock."; 17 | } 18 | break; 19 | case "openrouter": 20 | if (!apiConfiguration.openRouterApiKey) { 21 | return "You must provide a valid API key or choose a different provider."; 22 | } 23 | break; 24 | case "vertex": 25 | if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { 26 | return "You must provide a valid Google Cloud Project ID and Region."; 27 | } 28 | break; 29 | case "gemini": 30 | if (!apiConfiguration.geminiApiKey) { 31 | return "You must provide a valid API key or choose a different provider."; 32 | } 33 | break; 34 | case "openai-native": 35 | if (!apiConfiguration.openAiNativeApiKey) { 36 | return "You must provide a valid API key or choose a different provider."; 37 | } 38 | break; 39 | case "deepseek": 40 | if (!apiConfiguration.deepSeekApiKey) { 41 | return "You must provide a valid API key or choose a different provider."; 42 | } 43 | break; 44 | case "openai": 45 | if ( 46 | !apiConfiguration.openAiBaseUrl 47 | || !apiConfiguration.openAiApiKey 48 | || !apiConfiguration.openAiModelId 49 | ) { 50 | return "You must provide a valid base URL, API key, and model ID."; 51 | } 52 | break; 53 | case "ollama": 54 | if (!apiConfiguration.ollamaModelId) { 55 | return "You must provide a valid model ID."; 56 | } 57 | break; 58 | case "lmstudio": 59 | if (!apiConfiguration.lmStudioModelId) { 60 | return "You must provide a valid model ID."; 61 | } 62 | break; 63 | case "vscode-lm": 64 | if (!apiConfiguration.vsCodeLmModelSelector) { 65 | return "You must provide a valid model selector."; 66 | } 67 | break; 68 | } 69 | } 70 | return undefined; 71 | } 72 | 73 | export function validateModelId( 74 | apiConfiguration?: ApiConfiguration, 75 | openRouterModels?: Record 76 | ): string | undefined { 77 | if (apiConfiguration != null) { 78 | switch (apiConfiguration.apiProvider) { 79 | case "openrouter": 80 | const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId; // in case the user hasn't changed the model id, it will be undefined by default 81 | if (!modelId) { 82 | return "You must provide a model ID."; 83 | } 84 | if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) { 85 | // even if the model list endpoint failed, extensionstatecontext will always have the default model info 86 | return "The model ID you provided is not available. Please choose a different model."; 87 | } 88 | break; 89 | } 90 | } 91 | return undefined; 92 | } 93 | -------------------------------------------------------------------------------- /src/webview-ui/utils/vscode.ts: -------------------------------------------------------------------------------- 1 | import type { WebviewApi } from "vscode-webview"; 2 | 3 | import type { WebviewMessage } from "../../../src/shared/WebviewMessage"; 4 | 5 | 6 | /** 7 | * A utility wrapper around the acquireVsCodeApi() function, which enables 8 | * message passing and state management between the webview and extension 9 | * contexts. 10 | * 11 | * This utility also enables webview code to be run in a web browser-based 12 | * dev server by using native web browser features that mock the functionality 13 | * enabled by acquireVsCodeApi. 14 | */ 15 | class VSCodeAPIWrapper { 16 | private readonly vsCodeApi: WebviewApi | undefined; 17 | 18 | constructor() { 19 | // Check if the acquireVsCodeApi function exists in the current development 20 | // context (i.e. VS Code development window or web browser) 21 | if (typeof acquireVsCodeApi === "function") { 22 | this.vsCodeApi = acquireVsCodeApi(); 23 | } 24 | } 25 | 26 | /** 27 | * Get the persistent state stored for this webview. 28 | * 29 | * @remarks When running webview source code inside a web browser, getState will retrieve state 30 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 31 | * 32 | * @return The current state or `undefined` if no state has been set. 33 | */ 34 | public getState(): unknown | undefined { 35 | if (this.vsCodeApi) { 36 | return this.vsCodeApi.getState(); 37 | } 38 | else { 39 | const state = localStorage.getItem("vscodeState"); 40 | return state ? JSON.parse(state) : undefined; 41 | } 42 | } 43 | 44 | /** 45 | * Post a message (i.e. send arbitrary data) to the owner of the webview. 46 | * 47 | * @remarks When running webview code inside a web browser, postMessage will instead 48 | * log the given message to the console. 49 | * 50 | * @param message Abitrary data (must be JSON serializable) to send to the extension context. 51 | */ 52 | public postMessage(message: WebviewMessage) { 53 | if (this.vsCodeApi) { 54 | this.vsCodeApi.postMessage(message); 55 | } 56 | else { 57 | console.log(message); 58 | } 59 | } 60 | 61 | /** 62 | * Set the persistent state stored for this webview. 63 | * 64 | * @remarks When running webview source code inside a web browser, setState will set the given 65 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 66 | * 67 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved 68 | * using {@link getState}. 69 | * 70 | * @return The new state. 71 | */ 72 | public setState(newState: T): T { 73 | if (this.vsCodeApi) { 74 | return this.vsCodeApi.setState(newState); 75 | } 76 | else { 77 | localStorage.setItem("vscodeState", JSON.stringify(newState)); 78 | return newState; 79 | } 80 | } 81 | } 82 | 83 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi. 84 | export const vscodeApiWrapper = new VSCodeAPIWrapper(); 85 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | 4 | describe("should", () => { 5 | it("exported", () => { 6 | expect(1).toEqual(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "preact", 6 | "lib": [ 7 | "DOM", 8 | "ES2021" 9 | ], 10 | "useDefineForClassFields": true, 11 | "baseUrl": "./", 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "paths": { 15 | "react": ["./node_modules/preact/compat/"], 16 | "react-dom": ["./node_modules/preact/compat/"], 17 | "@shared/*": ["src/shared/*"], 18 | "@extension/*": ["src/extension/*"], 19 | "@webview-ui/*": ["src/webview-ui/*"] 20 | }, 21 | "resolveJsonModule": true, 22 | "types": [ 23 | "node", 24 | "vscode", 25 | "react", 26 | "react-dom", 27 | "@rsbuild/core/types" 28 | ], 29 | "allowImportingTsExtensions": true, 30 | "allowJs": true, 31 | "strict": true, 32 | "strictNullChecks": true, 33 | "noEmit": true, 34 | "outDir": "dist", 35 | "sourceMap": true, 36 | "sourceRoot": "src", 37 | "esModuleInterop": true, 38 | "isolatedModules": true, 39 | "skipDefaultLibCheck": true, 40 | "skipLibCheck": true 41 | }, 42 | "include": [ 43 | "rsbuild.config.ts", 44 | "src/**/*.ts", 45 | "src/**/*.tsx", 46 | "test/**/*.test.ts", 47 | "src/**/*.jsx" 48 | ], 49 | "exclude": [ 50 | "node_modules", 51 | "dist", 52 | ".vscode-test" 53 | ] 54 | } 55 | --------------------------------------------------------------------------------