diff --git a/.eslintrc.json b/.eslintrc.json index 5bd690f..edc8fea 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -96,7 +96,11 @@ "react/jsx-no-bind": "off", "react/react-in-jsx-scope": "off", "react/function-component-definition": "off", - "react/no-array-index-key": "off" + "react/no-array-index-key": "off", + "react/button-has-type": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/no-static-element-interactions": "off", + "react/jsx-no-constructed-context-values": "off" } } ] diff --git a/.vscode/settings.json b/.vscode/settings.json index 18a00b5..bed5a06 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "treeshake", "tsup", "undici", + "uuidv", "vitepress", "vsix", "Xclip", diff --git a/package.json b/package.json index 7b70c73..6a0bc0c 100644 --- a/package.json +++ b/package.json @@ -338,6 +338,7 @@ "@langchain/anthropic": "^0.2.14", "@langchain/core": "0.2.23", "@langchain/openai": "^0.2.6", + "@lexical/react": "^0.17.0", "@tomjs/vite-plugin-vscode": "^2.5.5", "@types/diff": "^5.2.1", "@types/fs-extra": "^11.0.4", @@ -378,6 +379,7 @@ "inquirer": "^9.3.4", "knip": "^5.27.2", "langchain": "^0.2.16", + "lexical": "^0.17.0", "lint-staged": "^15.2.9", "minimatch": "^9.0.5", "node-fetch": "^3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49959a8..d417885 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@langchain/openai': specifier: ^0.2.6 version: 0.2.6(langchain@0.2.16(@langchain/anthropic@0.2.14)(fast-xml-parser@4.4.1)(ignore@5.3.2)(openai@4.55.4(zod@3.23.8))(playwright@1.45.1)) + '@lexical/react': + specifier: ^0.17.0 + version: 0.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.18) '@tomjs/vite-plugin-vscode': specifier: ^2.5.5 version: 2.5.5(@swc/core@1.7.10)(postcss@8.4.40)(typescript@5.4.5)(vite@5.4.0(@types/node@22.2.0)(less@4.2.0)) @@ -155,6 +158,9 @@ importers: langchain: specifier: ^0.2.16 version: 0.2.16(@langchain/anthropic@0.2.14)(fast-xml-parser@4.4.1)(ignore@5.3.2)(openai@4.55.4(zod@3.23.8))(playwright@1.45.1) + lexical: + specifier: ^0.17.0 + version: 0.17.0 lint-staged: specifier: ^15.2.9 version: 15.2.9 @@ -1383,6 +1389,77 @@ packages: resolution: {integrity: sha512-cXWgKE3sdWLSqAa8ykbCcUsUF1Kyr5J3HOWYGuobhPEycXW4WI++d5DhzdpL238mzoEXTi90VqfSCra37l5YqA==} engines: {node: '>=18'} + '@lexical/clipboard@0.17.0': + resolution: {integrity: sha512-wYtC6VJhuSxUZc69VTU+vBgzB4HQqhve2hLrr3v+3tR2aimx3KnKphCCP1TexCntxpEnOTPXafEgpOW/EVQE+Q==} + + '@lexical/code@0.17.0': + resolution: {integrity: sha512-8zrgHzf27aYySfUVeSKw8YP/LkRlXHSwD03BKlkSZAb4HX/WC60SGmdXUhtyTIBucqe0pnuGsRYfR9euD0/tfw==} + + '@lexical/devtools-core@0.17.0': + resolution: {integrity: sha512-0ftqWsoCb96oTc8Ok+uvjGAXZpsN9oc6ml3d46BdufdZyxHXC4qU3YVoPfLkgAHzH+4fQlNypu7u3Ym3dZ2rJg==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/dragon@0.17.0': + resolution: {integrity: sha512-XSsrHVwhjBIVF9VN9MFm6Go8fquj5H/jlYuyNzemHq0tOli8NaoSovGc5q0LwXr88RPsuIt1jluazR7Q1+kxTQ==} + + '@lexical/hashtag@0.17.0': + resolution: {integrity: sha512-E6nSoz9haB6JypQtYxG5OYr36AHgam/FBMu77OWNl1KsJbkP8nInm+P22QFsNnEvs4Hk6/0FJ5g42+lTEnGmIg==} + + '@lexical/history@0.17.0': + resolution: {integrity: sha512-SfeUKAXf9pZpqee9rMOTt33V0J0p/AS9TZLT9Un9dU6wAaHfv6NFax1ND0JoG1a9YkTc539mufxVLNjsNRc0ag==} + + '@lexical/html@0.17.0': + resolution: {integrity: sha512-sI458CEP/j+Gd2YEo1+vTax31ZAjdq5jmRJMgSKxzKlkVYAUY9eH5u3Y3awPLwLVXJHiIopMX02GeZytibuTiw==} + + '@lexical/link@0.17.0': + resolution: {integrity: sha512-Kux6yvPit6y0ksPpwimv3seVrXAsggkqB6oT6oAVBaDpYuygVEwNDqg/rCTtB3mHQ4eeuU33mdK7MSXZ34bZRQ==} + + '@lexical/list@0.17.0': + resolution: {integrity: sha512-anDuSUykTv+lqyCwl1m+sThrB15OKCa00Eo68/d2HQSHDD3KNWgSx709dcR17bD9oT204yOhMJbQGywuzcEyGQ==} + + '@lexical/mark@0.17.0': + resolution: {integrity: sha512-Ynqh9KHXUcB9qLOTGC9s+bbWtawOwRStkeIeAugTqrwckyYWeDaePpyJ6IhBBJy1E1CfpiZn71NDeP+FuRjnXQ==} + + '@lexical/markdown@0.17.0': + resolution: {integrity: sha512-6IuJ2l5p/Ma+VBUIStIRXwTC01GEzx21gvqqywuqBUzAOiMr1oRM+DGsQgrzZrcjX+LzUlZ5ZgjuWtK8XKVAZw==} + + '@lexical/offset@0.17.0': + resolution: {integrity: sha512-onE6SD2mIAwBLTT5v5fVBVtRg/NpQj+o10vTWJ1ImvEUERpSoCyHMTy3IMoSMuCRwuOG9C0cFEret2u+QS8Icw==} + + '@lexical/overflow@0.17.0': + resolution: {integrity: sha512-dh+nQAmeobKvZFodWyzNh1ZjX043Patk/1Lwct9XmtAGMUdXL+tB0bbguWVcDfY8OYu1CTQGfbdq2oMEJYzwsg==} + + '@lexical/plain-text@0.17.0': + resolution: {integrity: sha512-AEk+3ttbRyRi7m9UbU1CdLUtGsXh4FFZkBC12twV3U82lZHOdHocLlTutP+lcbYlGjeq6UF43NxOSGzsYEunsA==} + + '@lexical/react@0.17.0': + resolution: {integrity: sha512-HZ3joq+5g2++2vo/6scTd60Y2bsu8ya8EUdopyudnmGZGKAcAPue9pLOlBaEpsYZ7vqTuGjiPgtEBfFzDy9rlg==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/rich-text@0.17.0': + resolution: {integrity: sha512-XJc8gQBSwppCkESQaNcGtyTaPXZaeCQDcUVpnDjDK0vM/ZZN8TErxbujwbSqA3kO2dBds9N8WxNboSwuncMBcQ==} + + '@lexical/selection@0.17.0': + resolution: {integrity: sha512-UTjlvyhFY/lmHtBaIaVRwYnRfO9gR4I32+PT7vHQr4v3VfcgS63YEGSgEZy3Gh1pfeJqaZATN58+jCuMAQXlWQ==} + + '@lexical/table@0.17.0': + resolution: {integrity: sha512-RQF7IG0rGL2/bPaPFUIMgDA3QMdDflvXSnE7Udgbj9yMqSKhYkaERVfNyoLckDUSuusGJd6XV+qum6JWn0nSNA==} + + '@lexical/text@0.17.0': + resolution: {integrity: sha512-kFH0V6yjW8YswmoY7vHT4zHFDflGfamuUxTPHROpdnq/JMjHeaVwtmFBdrP0gknaC8XMRXdr3EsemQ7cbOoDPA==} + + '@lexical/utils@0.17.0': + resolution: {integrity: sha512-B/n0rRGDmdMrqi2qnprLt6SntC6jb4JItLmPl8zDDdg7/HxMdLq3F93vogeiXQJn0mlNqgiENWHvLAy5K2C2uQ==} + + '@lexical/yjs@0.17.0': + resolution: {integrity: sha512-xJv3frcK/jskssLbzdY4yfBaM7+LWaZD4YjYkJ/bvRDTey2w+McF+SvsJ/yBA8YF1oaL3rT+0aIQJ7rfH+AxjA==} + peerDependencies: + yjs: '>=13.5.22' + '@microsoft/fast-element@1.13.0': resolution: {integrity: sha512-iFhzKbbD0cFRo9cEzLS3Tdo9BYuatdxmCEKCpZs1Cro/93zNMpZ/Y9/Z7SknmW6fhDZbpBvtO8lLh9TFEcNVAQ==} @@ -3595,6 +3672,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -3942,6 +4022,14 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lexical@0.17.0: + resolution: {integrity: sha512-cCFmANO5rIf34NF0go/hxp5S3V5Z8G2Rsa1FJy50qF2WM5EJNJ/MqN75TApjfgMkfrbO6gau3X12nCqwsT7aDg==} + + lib0@0.2.97: + resolution: {integrity: sha512-Q4d1ekgvufi9FiHkkL46AhecfNjznSL9MRNoJRQ76gBHS9OqU2ArfQK0FvBpuxgWeJeNI0LVgAYMIpsGeX4gYg==} + engines: {node: '>=16'} + hasBin: true + lilconfig@3.1.2: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} @@ -4599,6 +4687,10 @@ packages: resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} engines: {node: '>=18'} + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4632,6 +4724,12 @@ packages: peerDependencies: react: ^18.3.1 + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -5571,6 +5669,10 @@ packages: yazl@2.5.1: resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} + yjs@13.6.18: + resolution: {integrity: sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6795,6 +6897,151 @@ snapshots: - langchain - openai + '@lexical/clipboard@0.17.0': + dependencies: + '@lexical/html': 0.17.0 + '@lexical/list': 0.17.0 + '@lexical/selection': 0.17.0 + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/code@0.17.0': + dependencies: + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + prismjs: 1.29.0 + + '@lexical/devtools-core@0.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@lexical/html': 0.17.0 + '@lexical/link': 0.17.0 + '@lexical/mark': 0.17.0 + '@lexical/table': 0.17.0 + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@lexical/dragon@0.17.0': + dependencies: + lexical: 0.17.0 + + '@lexical/hashtag@0.17.0': + dependencies: + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/history@0.17.0': + dependencies: + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/html@0.17.0': + dependencies: + '@lexical/selection': 0.17.0 + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/link@0.17.0': + dependencies: + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/list@0.17.0': + dependencies: + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/mark@0.17.0': + dependencies: + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/markdown@0.17.0': + dependencies: + '@lexical/code': 0.17.0 + '@lexical/link': 0.17.0 + '@lexical/list': 0.17.0 + '@lexical/rich-text': 0.17.0 + '@lexical/text': 0.17.0 + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/offset@0.17.0': + dependencies: + lexical: 0.17.0 + + '@lexical/overflow@0.17.0': + dependencies: + lexical: 0.17.0 + + '@lexical/plain-text@0.17.0': + dependencies: + '@lexical/clipboard': 0.17.0 + '@lexical/selection': 0.17.0 + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/react@0.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.18)': + dependencies: + '@lexical/clipboard': 0.17.0 + '@lexical/code': 0.17.0 + '@lexical/devtools-core': 0.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@lexical/dragon': 0.17.0 + '@lexical/hashtag': 0.17.0 + '@lexical/history': 0.17.0 + '@lexical/link': 0.17.0 + '@lexical/list': 0.17.0 + '@lexical/mark': 0.17.0 + '@lexical/markdown': 0.17.0 + '@lexical/overflow': 0.17.0 + '@lexical/plain-text': 0.17.0 + '@lexical/rich-text': 0.17.0 + '@lexical/selection': 0.17.0 + '@lexical/table': 0.17.0 + '@lexical/text': 0.17.0 + '@lexical/utils': 0.17.0 + '@lexical/yjs': 0.17.0(yjs@13.6.18) + lexical: 0.17.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-error-boundary: 3.1.4(react@18.3.1) + transitivePeerDependencies: + - yjs + + '@lexical/rich-text@0.17.0': + dependencies: + '@lexical/clipboard': 0.17.0 + '@lexical/selection': 0.17.0 + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/selection@0.17.0': + dependencies: + lexical: 0.17.0 + + '@lexical/table@0.17.0': + dependencies: + '@lexical/utils': 0.17.0 + lexical: 0.17.0 + + '@lexical/text@0.17.0': + dependencies: + lexical: 0.17.0 + + '@lexical/utils@0.17.0': + dependencies: + '@lexical/list': 0.17.0 + '@lexical/selection': 0.17.0 + '@lexical/table': 0.17.0 + lexical: 0.17.0 + + '@lexical/yjs@0.17.0(yjs@13.6.18)': + dependencies: + '@lexical/offset': 0.17.0 + lexical: 0.17.0 + yjs: 13.6.18 + '@microsoft/fast-element@1.13.0': {} '@microsoft/fast-foundation@2.49.6': @@ -9489,6 +9736,8 @@ snapshots: isexe@2.0.0: {} + isomorphic.js@0.2.5: {} + iterator.prototype@1.1.2: dependencies: define-properties: 1.2.1 @@ -9709,6 +9958,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lexical@0.17.0: {} + + lib0@0.2.97: + dependencies: + isomorphic.js: 0.2.5 + lilconfig@3.1.2: {} lines-and-columns@1.2.4: {} @@ -10330,6 +10585,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + prismjs@1.29.0: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10369,6 +10626,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-error-boundary@3.1.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.7 + react: 18.3.1 + react-is@16.13.1: {} react@18.3.1: @@ -11404,6 +11666,10 @@ snapshots: dependencies: buffer-crc32: 0.2.13 + yjs@13.6.18: + dependencies: + lib0: 0.2.97 + yocto-queue@0.1.0: {} yocto-queue@1.1.1: {} diff --git a/src/extension/registers/webview.register.ts b/src/extension/registers/webview.register.ts index 8588373..fd9635e 100644 --- a/src/extension/registers/webview.register.ts +++ b/src/extension/registers/webview.register.ts @@ -1,5 +1,6 @@ import { setupHtml } from '@extension/utils' import { setupWebviewAPIManager } from '@extension/webview-api' +import type { WebviewPanel } from '@extension/webview-api/types' import * as vscode from 'vscode' import { BaseRegister } from './base.register' @@ -49,9 +50,7 @@ export class AideWebViewProvider { } } - private async setupWebview( - webview: vscode.WebviewView | vscode.WebviewPanel - ) { + private async setupWebview(webview: WebviewPanel) { this.cleanUp() const setupWebviewAPIManagerDispose = await setupWebviewAPIManager( diff --git a/src/extension/webview-api/api-manager.ts b/src/extension/webview-api/api-manager.ts deleted file mode 100644 index 20eb374..0000000 --- a/src/extension/webview-api/api-manager.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -import { logger } from '@extension/logger' -import * as vscode from 'vscode' - -import { - REQUEST_CLEANUP_INTERVAL, - REQUEST_CLEANUP_THRESHOLD, - REQUEST_TIMEOUT -} from './constant' -import type { BaseController } from './controllers/base.controller' -import { - APIError, - APIHandler, - APIMethodMap, - Controller, - type WebviewPanel -} from './types' - -export class APIManager { - private handlers: Record = {} - - private streamHandlers: Record< - string, - (sessionId: string, data: any) => void - > = {} - - private controllers: Record = {} - - private panel: WebviewPanel - - private pendingRequests: Map< - string, - { timestamp: number; reject: (reason?: any) => void } - > = new Map() - - private disposes: vscode.Disposable[] = [] - - constructor( - private context: vscode.ExtensionContext, - panel: WebviewPanel - ) { - this.panel = panel - this.setupMessageListener() - this.startRequestCleaner() - } - - registerController( - ControllerClass: new ( - ...args: ConstructorParameters - ) => BaseController - ) { - const controller = new ControllerClass(this.context, this) - this.controllers[controller.name] = controller - Object.entries(controller.handlers).forEach(([key, handler]) => { - this.handlers[`${controller.name}.${key}`] = handler.bind(controller) - }) - if (controller.streamHandlers) { - Object.entries(controller.streamHandlers).forEach(([key, handler]) => { - this.streamHandlers[`${controller.name}.${key}`] = - handler.bind(controller) - }) - } - } - - private setupMessageListener() { - const onDidReceiveMessageDispose = this.panel.webview.onDidReceiveMessage( - async (message: any) => { - const { id, sessionId, command, params } = message - if (this.pendingRequests.has(id)) { - this.sendErrorToWebview( - id, - sessionId, - 'DUPLICATE_REQUEST', - 'Duplicate request ID' - ) - return - } - - const handler = this.handlers[command] - if (handler) { - const timeoutPromise = this.createTimeout(id, REQUEST_TIMEOUT) - - this.pendingRequests.set(id, { - timestamp: Date.now(), - reject: timeoutPromise.reject - }) - - try { - const result = await Promise.race([ - // @ts-ignore - handler(sessionId, params), - timeoutPromise.promise - ]) - await this.sendResultToWebview(id, sessionId, result) - } catch (error) { - await this.handleError(id, sessionId, command, error) - } finally { - this.pendingRequests.delete(id) - } - } else { - await this.sendErrorToWebview( - id, - sessionId, - 'HANDLER_NOT_FOUND', - `Handler not found: ${command}` - ) - } - } - ) - - this.disposes.push(onDidReceiveMessageDispose) - } - - private createTimeout( - id: string, - ms: number - ): { promise: Promise; reject: (reason?: any) => void } { - let reject: (reason?: any) => void - - const promise = new Promise((_, rej) => { - reject = rej - const timer = setTimeout( - () => rej(new APIError('TIMEOUT', 'Request timed out')), - ms - ) - - this.disposes.push({ - dispose: () => clearTimeout(timer) - }) - }) - return { promise, reject: reject! } - } - - private async sendResultToWebview( - id: string, - sessionId: string, - result: any - ) { - await this.panel.webview.postMessage({ id, sessionId, result }) - } - - private async sendErrorToWebview( - id: string, - sessionId: string, - code: string, - message: string, - details?: any - ) { - await this.panel.webview.postMessage({ - id, - sessionId, - error: { code, message, details } - }) - } - - private async handleError( - id: string, - sessionId: string, - command: string, - error: any - ) { - logger.warn(`Error in handler for ${command}:`, error) - if (error instanceof APIError) { - await this.sendErrorToWebview( - id, - sessionId, - error.code, - error.message, - error.details - ) - } else { - await this.sendErrorToWebview( - id, - sessionId, - 'INTERNAL_ERROR', - 'An unexpected error occurred', - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error) - ) - } - } - - async sendToWebview(command: string, sessionId: string, data: any) { - const streamHandler = this.streamHandlers[command] - - if (streamHandler) { - streamHandler(sessionId, data) - } - - await this.panel.webview.postMessage({ command, sessionId, data }) - } - - async callHandler( - command: `${string & C}.${string & M}`, - sessionId: string, - params: T[C][M]['params'] - ): Promise { - const handler = this.handlers[command] - - if (handler) { - try { - // @ts-ignore - return (await handler(sessionId, params)) as T[C][M]['result'] - } catch (error) { - logger.warn(`Error in handler ${command}:`, error) - throw error - } - } - - throw new APIError('HANDLER_NOT_FOUND', `Handler not found: ${command}`) - } - - private startRequestCleaner() { - const timer = setInterval(() => { - const now = Date.now() - - for (const [ - id, - { timestamp, reject } - ] of this.pendingRequests.entries()) { - if (now - timestamp > REQUEST_CLEANUP_THRESHOLD) { - reject( - new APIError('TIMEOUT', 'Request timed out and was cleaned up') - ) - this.pendingRequests.delete(id) - } - } - }, REQUEST_CLEANUP_INTERVAL) - - this.disposes.push({ - dispose: () => clearInterval(timer) - }) - } - - cleanUp() { - this.disposes.forEach(dispose => dispose.dispose()) - } -} diff --git a/src/extension/webview-api/chat-context-builder/chat-context-builder.ts b/src/extension/webview-api/chat-context-builder/chat-context-builder.ts deleted file mode 100644 index d075bd0..0000000 --- a/src/extension/webview-api/chat-context-builder/chat-context-builder.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getErrorMsg } from '@extension/utils' - -import { ChatContextBuilderError, PluginError } from './error' -import type { PluginManager } from './plugin-manager' -import type { ChatContext } from './types/chat-context' -import type { LangchainMessageType } from './types/langchain-message' - -export class ChatContextBuilder { - constructor(public pluginManager: PluginManager) {} - - async buildContext( - context: Partial - ): Promise { - try { - let messages: LangchainMessageType[] = [] - - for await (const plugin of this.pluginManager.getAllPlugins()) { - messages = messages.concat( - await plugin.buildContext(context, this.pluginManager) - ) - } - - return messages - } catch (error) { - const errMsg = getErrorMsg(error) - - if (error instanceof PluginError) { - throw error - } else { - // Handle general errors - throw new ChatContextBuilderError(`Failed to build context: ${errMsg}`) - } - } - } -} diff --git a/src/extension/webview-api/chat-context-builder/error.ts b/src/extension/webview-api/chat-context-builder/error.ts deleted file mode 100644 index 853c19b..0000000 --- a/src/extension/webview-api/chat-context-builder/error.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class PluginError extends Error { - constructor(pluginName: string, message: string) { - super(`[${pluginName}] ${message}`) - this.name = 'ChatContextBuilderPluginError' - } -} - -export class ChatContextBuilderError extends Error { - constructor(message: string) { - super(message) - this.name = 'ChatContextBuilderError' - } -} diff --git a/src/extension/webview-api/chat-context-builder/index.ts b/src/extension/webview-api/chat-context-builder/index.ts deleted file mode 100644 index 00fd7f5..0000000 --- a/src/extension/webview-api/chat-context-builder/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ChatContextBuilder } from './chat-context-builder' -import { PluginManager } from './plugin-manager' -import { CodeChunksPlugin } from './plugins/code-chunks.plugin' -import { ConversationPlugin } from './plugins/conversation.plugin' -import { CurrentFilePlugin } from './plugins/current-file.plugin' -import { ExplicitContextPlugin } from './plugins/explicit-context.plugin' -import { GitPlugin } from './plugins/git.plugin' - -export const createChatContextBuilder = - async (): Promise => { - const pluginManager = new PluginManager() - await pluginManager.registerPlugin(new CurrentFilePlugin()) - await pluginManager.registerPlugin(new ConversationPlugin()) - await pluginManager.registerPlugin(new CodeChunksPlugin()) - await pluginManager.registerPlugin(new ExplicitContextPlugin()) - await pluginManager.registerPlugin(new GitPlugin()) - - const chatContextBuilder = new ChatContextBuilder(pluginManager) - - return chatContextBuilder - } diff --git a/src/extension/webview-api/chat-context-builder/plugin-manager.ts b/src/extension/webview-api/chat-context-builder/plugin-manager.ts deleted file mode 100644 index 0e15f0b..0000000 --- a/src/extension/webview-api/chat-context-builder/plugin-manager.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { BasePlugin } from './plugins/base.plugin' - -export class PluginManager { - private plugins: Map = new Map() - - async registerPlugin(plugin: BasePlugin): Promise { - await plugin.initialize() - this.plugins.set(plugin.name, plugin) - } - - getPlugin(name: string): BasePlugin | undefined { - return this.plugins.get(name) - } - - getAllPlugins(): BasePlugin[] { - return Array.from(this.plugins.values()) - } - - async cleanupPlugin(name: string): Promise { - const plugin = this.plugins.get(name) - if (plugin) { - await plugin.cleanup() - this.plugins.delete(name) - } - } - - async cleanupAllPlugins(): Promise { - for (const plugin of this.plugins.values()) { - await plugin.cleanup() - } - this.plugins.clear() - } -} diff --git a/src/extension/webview-api/chat-context-builder/plugins/base.plugin.ts b/src/extension/webview-api/chat-context-builder/plugins/base.plugin.ts deleted file mode 100644 index c320dff..0000000 --- a/src/extension/webview-api/chat-context-builder/plugins/base.plugin.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getErrorMsg } from '@extension/utils' - -import { PluginError } from '../error' -import type { PluginManager } from '../plugin-manager' -import type { ChatContext } from '../types/chat-context' -import type { LangchainMessageType } from '../types/langchain-message' - -export abstract class BasePlugin { - abstract name: string - - async initialize(): Promise { - // Default implementation - } - - abstract buildContext( - context: Partial, - pluginManager: PluginManager - ): Promise - - protected createError(error: unknown): Error { - const errMsg = getErrorMsg(error) - return new PluginError(this.name, errMsg) - } - - async cleanup(): Promise { - // Default implementation - } -} diff --git a/src/extension/webview-api/chat-context-builder/plugins/code-chunks.plugin.ts b/src/extension/webview-api/chat-context-builder/plugins/code-chunks.plugin.ts deleted file mode 100644 index bab91e9..0000000 --- a/src/extension/webview-api/chat-context-builder/plugins/code-chunks.plugin.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { HumanMessage, SystemMessage } from '@langchain/core/messages' - -import type { ChatContext } from '../types/chat-context' -import type { LangchainMessageType } from '../types/langchain-message' -import { BasePlugin } from './base.plugin' - -export class CodeChunksPlugin extends BasePlugin { - name = 'CodeChunks' - - async buildContext( - context: Partial - ): Promise { - const conversation = context.conversation || [] - - const codeChunks = conversation - .filter(msg => msg.type === 'human') - .flatMap(msg => msg.attachedCodeChunks || []) - - if (codeChunks.length === 0) return [] - - const chunksContent = codeChunks - .map( - chunk => - `\`\`\`${chunk.languageIdentifier}:${chunk.relativeWorkspacePath}\n${chunk.lines.join( - '\n' - )}\n\`\`\`` - ) - .join('\n\n') - - return [ - new SystemMessage('Relevant code chunks:'), - new HumanMessage(chunksContent) - ] - } -} diff --git a/src/extension/webview-api/chat-context-builder/plugins/conversation.plugin.ts b/src/extension/webview-api/chat-context-builder/plugins/conversation.plugin.ts deleted file mode 100644 index 50ad1ce..0000000 --- a/src/extension/webview-api/chat-context-builder/plugins/conversation.plugin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AIMessage, HumanMessage } from '@langchain/core/messages' - -import type { ChatContext } from '../types/chat-context' -import type { LangchainMessageType } from '../types/langchain-message' -import { BasePlugin } from './base.plugin' - -export class ConversationPlugin extends BasePlugin { - name = 'Conversation' - - async buildContext( - context: Partial - ): Promise { - if (!context.conversation || context.conversation.length === 0) return [] - - return context.conversation.map(msg => { - if (msg.type === 'human') { - return new HumanMessage(msg.text) - } - return new AIMessage(msg.text) - }) - } -} diff --git a/src/extension/webview-api/chat-context-builder/plugins/current-file.plugin.ts b/src/extension/webview-api/chat-context-builder/plugins/current-file.plugin.ts deleted file mode 100644 index 483c639..0000000 --- a/src/extension/webview-api/chat-context-builder/plugins/current-file.plugin.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ChatContext } from '../types/chat-context' -import type { LangchainMessageType } from '../types/langchain-message' -import { BasePlugin } from './base.plugin' - -export class CurrentFilePlugin extends BasePlugin { - name = 'CurrentFile' - - async buildContext( - // eslint-disable-next-line unused-imports/no-unused-vars - context: Partial - ): Promise { - return [] - } -} diff --git a/src/extension/webview-api/chat-context-builder/plugins/explicit-context.plugin.ts b/src/extension/webview-api/chat-context-builder/plugins/explicit-context.plugin.ts deleted file mode 100644 index 8f51a43..0000000 --- a/src/extension/webview-api/chat-context-builder/plugins/explicit-context.plugin.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SystemMessage } from '@langchain/core/messages' - -import type { ChatContext } from '../types/chat-context' -import type { LangchainMessageType } from '../types/langchain-message' -import { BasePlugin } from './base.plugin' - -export class ExplicitContextPlugin extends BasePlugin { - name = 'ExplicitContext' - - async buildContext( - context: Partial - ): Promise { - if (!context.explicitContext?.context) return [] - - return [new SystemMessage(context.explicitContext.context)] - } -} diff --git a/src/extension/webview-api/chat-context-builder/plugins/git.plugin.ts b/src/extension/webview-api/chat-context-builder/plugins/git.plugin.ts deleted file mode 100644 index 74bb5e6..0000000 --- a/src/extension/webview-api/chat-context-builder/plugins/git.plugin.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { SystemMessage } from '@langchain/core/messages' - -import type { ChatContext } from '../types/chat-context' -import type { Message } from '../types/chat-context/message' -import type { LangchainMessageType } from '../types/langchain-message' -import { BasePlugin } from './base.plugin' - -export class GitPlugin extends BasePlugin { - name = 'Git' - - async buildContext( - context: Partial - ): Promise { - const conversation = context.conversation || [] - const gitInfo: string[] = [] - - this.addCommits(conversation, gitInfo) - this.addPullRequests(conversation, gitInfo) - this.addGitDiffs(conversation, gitInfo) - - if (gitInfo.length === 0) return [] - - return [new SystemMessage(gitInfo.join('\n'))] - } - - private addCommits(conversation: Message[], gitInfo: string[]) { - const commits = this.extractFromConversation(conversation, 'commits') - if (commits.length > 0) { - gitInfo.push('Relevant commits:') - commits.forEach(commit => { - gitInfo.push(`- ${JSON.stringify(commit)}`) - }) - } - } - - private addPullRequests(conversation: Message[], gitInfo: string[]) { - const prs = this.extractFromConversation(conversation, 'pullRequests') - if (prs.length > 0) { - gitInfo.push('Relevant pull requests:') - prs.forEach(pr => { - gitInfo.push(`- ${JSON.stringify(pr)}`) - }) - } - } - - private addGitDiffs(conversation: Message[], gitInfo: string[]) { - const diffs = this.extractFromConversation(conversation, 'gitDiffs') - if (diffs.length > 0) { - gitInfo.push('Git diffs:') - diffs.forEach(diff => { - gitInfo.push(`- ${JSON.stringify(diff)}`) - }) - } - } - - private extractFromConversation( - conversation: Message[], - key: K - ): Message[K][] { - return conversation - .filter(msg => msg.type === 'human') - .flatMap(msg => msg[key]) - } -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/code-block.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/code-block.ts deleted file mode 100644 index 7d89e2c..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/code-block.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { FileUri } from './file-uri' - -export interface CodeBlock { - uri: FileUri - version: number -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/file-uri.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/file-uri.ts deleted file mode 100644 index 1bfff8c..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/file-uri.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface FileUri { - /** - * @example '/Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts' - */ - fsPath: string - - /** - * @example 'file:///Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts' - */ - external: string - - /** - * @example '/Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts' - */ - path: string - - /** - * @example 'file' - */ - scheme: string -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/index.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/index.ts deleted file mode 100644 index c1a342e..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { CodeBlock } from './code-block' -import type { FileUri } from './file-uri' -import type { Message } from './message' -import type { RichText } from './rich-text' - -export interface IFileContext { - focusedFiles: { - uri: FileUri - fileName: string - }[] - suggestedFiles: any[] - newlyCreatedFiles: { - uri: FileUri - }[] - newlyCreatedFolders: any[] - deleteFileSuggestions: any[] - isReadingLongFile: boolean - hasAddedFiles: boolean - codeBlockData: Record< - string, - { - /** - * @example 0 - */ - version: number - - /** - * @example { - * fsPath: - * '/Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts', - * external: - * 'file:///Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts', - * path: '/Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts', - * scheme: 'file' - * } - */ - predictedUri: FileUri - - /** - * @example { - * fsPath: - * '/Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts', - * external: - * 'file:///Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts', - * path: '/Users/xxx/Documents/codes/aide/src/extension/file-utils/create-should-ignore.ts', - * scheme: 'file' - */ - uri: FileUri - - /** - * @example 'completed' - */ - status: string - - /** - * @example [ - * "export const createShouldIgnore = (file: string) => {", - * " return file.startsWith('.') || file.startsWith('node_modules')", - * "}" - * ] - */ - newModelLines: string[] - - /** - * @example [" "] - */ - originalModelLines: string[] - }[] - > -} - -export interface IConversationContext { - conversation: Message[] - references: { - selections: any[] - fileSelections: any[] - folderSelections: any[] - useWeb: boolean - useCodebase: boolean - } - codeSelections: any[] - richText?: RichText - plainText: string -} - -export interface ISettingsContext { - modelName: string - useFastApply: boolean - fastApplyModelName?: string - useChunkSpeculationForLongFiles: boolean - explicitContext: { - /** - * Explicit context provided. - * @example '总是说中文' - */ - context: string - } - clickedCodeBlockContents: string - allowLongFileScan: boolean -} - -export interface IBaseContext { - tabs: - | { - type: 'composer' - } - | { - type: 'code' - codeBlocks: CodeBlock[] - }[] - - createdAt: number - lastUpdatedAt: number -} - -export interface ChatContext - extends IBaseContext, - IFileContext, - IConversationContext, - ISettingsContext {} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/message/assistant-suggestions.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/message/assistant-suggestions.ts deleted file mode 100644 index 6597271..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/message/assistant-suggestions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface AssistantSuggestions { - assistantSuggestedDiffs: any[] -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/message/attachment-info.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/message/attachment-info.ts deleted file mode 100644 index 5347f1b..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/message/attachment-info.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface AttachmentInfo { - /** - * @example ['/src/webview/types'] - */ - attachedFolders: string[] - attachedFoldersNew: string[] - images: string[] -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/message/basic-message.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/message/basic-message.ts deleted file mode 100644 index 204fdfc..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/message/basic-message.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { MessageType } from '@langchain/core/messages' - -import type { RichText } from '../rich-text' - -export interface BasicMessage { - /** - * @example - * '@index.ts @utils.ts @absolutePath @Web @vscode @ci: fix ci @types 优化一下' - */ - text: string - - richText?: RichText - - /** - * @example 'human' - */ - type: MessageType - - /** - * @example 'dd62428a-94d7-4cbe-a7fb-2b5a2510afg' - */ - bubbleId: string -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/message/code-related-info.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/message/code-related-info.ts deleted file mode 100644 index 06771ed..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/message/code-related-info.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { CodeBlock } from '../code-block' - -export interface CodeChunk { - /** - * Relative path of the code chunk in the workspace. - * @example 'src/extension/index.ts' - */ - relativeWorkspacePath: string - - /** - * Start line number of the code chunk. - * @example 1 - */ - startLineNumber: number - - /** - * Lines of code in the chunk. - * @example [ - * "export const sleep = (ms: number) =>', - * " new Promise(resolve => setTimeout(resolve, ms))", - * ] - */ - lines: string[] - - /** - * Strategy used for summarizing the code chunk. - * @example 'SUMMARIZATION_STRATEGY_NONE_UNSPECIFIED' - */ - summarizationStrategy: string - - /** - * Language identifier of the code chunk. - * @example 'typescript' - */ - languageIdentifier: string -} - -export interface CodebaseContextChunk { - /** - * @example 'src/webview/types/vscode.d.ts' - */ - relativeWorkspacePath: string - - range: { - startPosition: { - /** - * @example 1 - */ - line: number - - /** - * @example 1 - */ - column: number - } - endPosition: { - /** - * @example 10 - */ - line: number - - /** - * @example 2 - */ - column: number - } - } - - /** - * @example "import type { WebviewToExtensionsMsg } from '@shared/types'\n\ndeclare global {\n interface Window {\n acquireVsCodeApi(): {\n postMessage(msg: WebviewToExtensionsMsg): void\n setState(state: any): void\n getState(): any\n }\n vscode: ReturnType\n }\n}" - */ - contents: string - detailedLines: { - /** - * @example 'declare global {' - */ - text: string - - /** - * @example 3 - */ - lineNumber: number - - /** - * @example false - */ - isSignature: boolean - }[] -} - -export interface CodeRelatedInfo { - attachedCodeChunks: CodeChunk[] - codebaseContextChunks: CodebaseContextChunk[] - - /** - * modify files by ai - */ - codeBlocks?: CodeBlock[] - userModificationsToSuggestedCodeBlocks: any[] -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/message/git-related-info.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/message/git-related-info.ts deleted file mode 100644 index d8358a1..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/message/git-related-info.ts +++ /dev/null @@ -1,54 +0,0 @@ -export interface GitCommit { - /** - * @example '0bc7f06aa2930c2755c751615cfb2331de41ddb1' - */ - sha: string - - /** - * @example 'ci: fix ci' - */ - message: string - - /** - * @example '' - */ - description: string - diff: { - /** - * @example '.github/workflows/ci.yml' - */ - from: string - - /** - * @example '.github/workflows/ci.yml' - */ - to: string - chunks: { - /** - * @example '@@ -1,6 +1,6 @@ importers:' - */ - content: string - - /** - * @example [ - * 'name: CI', - * 'on:', - * ' push:', - * '+ branches:', - * '+ - main', - * '- branches:', - * '- - master', - * ] - */ - lines: string[] - }[] - }[] - author: string - date: string -} - -export interface GitRelatedInfo { - commits: GitCommit[] - pullRequests: any[] - gitDiffs: any[] -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/message/index.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/message/index.ts deleted file mode 100644 index 70340a7..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/message/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AssistantSuggestions } from './assistant-suggestions' -import type { AttachmentInfo } from './attachment-info' -import type { BasicMessage } from './basic-message' -import type { CodeRelatedInfo } from './code-related-info' -import type { GitRelatedInfo } from './git-related-info' -import type { InterpreterInfo } from './interpreter-info' - -export interface Message - extends BasicMessage, - CodeRelatedInfo, - GitRelatedInfo, - AssistantSuggestions, - InterpreterInfo, - AttachmentInfo {} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/message/interpreter-info.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/message/interpreter-info.ts deleted file mode 100644 index 46d87dd..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/message/interpreter-info.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface InterpreterInfo { - interpreterResults: any[] -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/index.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/index.ts deleted file mode 100644 index 3c71e5b..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Mention } from './mention' - -export interface RichTextTextNode { - detail: number - format: number - mode: 'normal' | 'segmented' - style: string - text: string - type: 'text' - version: number -} - -export type RichTextContentNode = Mention | RichTextTextNode - -export interface RichTextParagraph { - children: RichTextContentNode[] - direction: 'ltr' - format: string - indent: number - type: 'paragraph' - version: number -} - -export interface RichTextRootNode { - children: RichTextParagraph[] - direction: 'ltr' - format: string - indent: number - type: 'root' - version: number -} - -export interface RichText { - root: RichTextRootNode -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/mention-type.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/mention-type.ts deleted file mode 100644 index 1a496c2..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/mention-type.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum MentionType { - File = 'file', - Folder = 'folder', - Code = 'code', - Web = 'web', - Doc = 'doc', - Diffs = 'diffs', - Commits = 'commits', - Codebase = 'codebase' -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/mention.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/mention.ts deleted file mode 100644 index 03410f4..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/mention.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Metadata } from './metadata' - -export interface Mention { - detail: number - format: number - mode: 'segmented' - style: string - text: string - type: 'mention' - version: number - mentionName: string - storedKey: string - metadata: Metadata -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/metadata.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/metadata.ts deleted file mode 100644 index 8e42cf0..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/metadata.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { MentionType } from './mention-type' -import type { SelectionType } from './selection-type' - -export interface Metadata { - selection: { - type: SelectionType - selectionWithoutUuid?: any // This could be further typed based on specific selection types - } - selectedOption: { - key: string - type: MentionType - score: number - name: string - picture: Record - secondaryText?: string - selectionPrecursor?: any - docSelection?: { - docId: string - name: string - url: string - } - diff?: string - } -} diff --git a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/selection-type.ts b/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/selection-type.ts deleted file mode 100644 index 2df6b50..0000000 --- a/src/extension/webview-api/chat-context-builder/types/chat-context/rich-text/selection-type.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum SelectionType { - None = 0, - CodeSelection = 1, - FileSelection = 2, - // Add other selection types as needed - FolderSelection = 5, - DocSelection = 6 -} diff --git a/src/extension/webview-api/chat-context-builder/types/langchain-message.ts b/src/extension/webview-api/chat-context-builder/types/langchain-message.ts deleted file mode 100644 index 095fae1..0000000 --- a/src/extension/webview-api/chat-context-builder/types/langchain-message.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { - AIMessage, - HumanMessage, - SystemMessage -} from '@langchain/core/messages' - -export type LangchainMessageType = HumanMessage | SystemMessage | AIMessage diff --git a/src/extension/webview-api/chat-context-processor/core/attachment-processor.ts b/src/extension/webview-api/chat-context-processor/core/attachment-processor.ts new file mode 100644 index 0000000..f3e2a00 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/core/attachment-processor.ts @@ -0,0 +1,58 @@ +import { logger } from '@extension/logger' + +import type { ChatContext } from '../types/chat-context' +import type { + Attachments, + Conversation +} from '../types/chat-context/conversation' +import type { ProcessorRegistry } from './processor-registry' + +export class AttachmentProcessor { + constructor(private processorRegistry: ProcessorRegistry) {} + + async processAttachments( + conversation: Conversation, + context: ChatContext + ): Promise { + const attachmentResults = await Promise.allSettled( + Object.entries(conversation.attachments).map(([key, attachment]) => + this.processAttachment( + key as keyof Attachments, + attachment, + conversation, + context + ) + ) + ) + + return attachmentResults.reduce((acc, result) => { + if (result.status === 'fulfilled' && result.value) { + return acc + result.value + } + return acc + }, '') + } + + private async processAttachment( + key: keyof Attachments, + attachment: any, + conversation: Conversation, + context: ChatContext + ): Promise { + const processor = this.processorRegistry.get(key) + if (processor && attachment) { + try { + const params = await processor.buildMessageParams( + attachment, + conversation, + context + ) + return typeof params === 'string' ? params : (params.content as string) + } catch (error) { + logger.warn(`Error processing attachment ${key}:`, error) + return null + } + } + return null + } +} diff --git a/src/extension/webview-api/chat-context-processor/core/content-manager.ts b/src/extension/webview-api/chat-context-processor/core/content-manager.ts new file mode 100644 index 0000000..35012bf --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/core/content-manager.ts @@ -0,0 +1,47 @@ +import type { + MessageContentComplex, + MessageContentImageUrl, + MessageContentText +} from '@langchain/core/messages' + +import type { LangchainMessageParams } from '../types/langchain-message' + +export class ContentManager { + private messageContent: MessageContentComplex[] + + constructor() { + this.messageContent = [{ type: 'text', text: '' }] + } + + appendText(text: string): void { + const textContent = this.messageContent[0] as MessageContentText + textContent.text += text + } + + appendImage(url: string): void { + this.messageContent.push({ + type: 'image_url', + image_url: url + } as MessageContentImageUrl) + } + + mergeMessageParams(params: LangchainMessageParams): void { + if (typeof params === 'string') { + this.appendText(`\n${params}`) + } else if (typeof params.content === 'string') { + this.appendText(`\n${params.content}`) + } else { + params.content.forEach(content => { + if (content.type === 'text') { + this.appendText(content.text) + } else if (content.type === 'image_url') { + this.appendImage(content.image_url) + } + }) + } + } + + getContent(): MessageContentComplex[] { + return this.messageContent + } +} diff --git a/src/extension/webview-api/chat-context-processor/core/message-builder.ts b/src/extension/webview-api/chat-context-processor/core/message-builder.ts new file mode 100644 index 0000000..4058976 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/core/message-builder.ts @@ -0,0 +1,29 @@ +import { + AIMessage, + HumanMessage, + SystemMessage, + type MessageType +} from '@langchain/core/messages' + +import type { + LangchainMessage, + LangchainMessageParams +} from '../types/langchain-message' + +export class MessageBuilder { + static createMessage( + type: MessageType, + messageParams: LangchainMessageParams + ): LangchainMessage { + switch (type) { + case 'human': + return new HumanMessage(messageParams) + case 'ai': + return new AIMessage(messageParams) + case 'system': + return new SystemMessage(messageParams) + default: + throw new Error(`Unsupported message type: ${type}`) + } + } +} diff --git a/src/extension/webview-api/chat-context-processor/core/processor-registry.ts b/src/extension/webview-api/chat-context-processor/core/processor-registry.ts new file mode 100644 index 0000000..6ef818e --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/core/processor-registry.ts @@ -0,0 +1,28 @@ +import type { Attachments as IAttachments } from '../types/chat-context/conversation' +import type { ContextProcessor } from '../types/context-processor' + +export class ProcessorRegistry< + AttachmentName extends keyof IAttachments = keyof IAttachments +> { + private processors: Map< + AttachmentName, + ContextProcessor + > = new Map() + + register( + name: AttachmentName, + processor: ContextProcessor + ): void { + this.processors.set(name, processor) + } + + get( + name: AttachmentName + ): ContextProcessor | undefined { + return this.processors.get(name) + } + + entries(): IterableIterator<[AttachmentName, ContextProcessor]> { + return this.processors.entries() + } +} diff --git a/src/extension/webview-api/chat-context-processor/core/tool-manager.ts b/src/extension/webview-api/chat-context-processor/core/tool-manager.ts new file mode 100644 index 0000000..35b4dd7 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/core/tool-manager.ts @@ -0,0 +1,100 @@ +import { tryParseJSON } from '@extension/utils' +import type { ToolCall } from '@langchain/core/dist/messages/tool' +import { tool, type DynamicStructuredTool } from '@langchain/core/tools' + +import type { ChatContext } from '../types/chat-context' +import type { BaseToolContext } from '../types/chat-context/base-tool-context' +import type { Conversation } from '../types/chat-context/conversation' +import type { ContextProcessor } from '../types/context-processor' +import type { AIModel, ToolsInfoMap } from '../types/core' +import type { ProcessorRegistry } from './processor-registry' + +export class ToolManager { + async buildConversationToolsInfoMap( + context: ChatContext, + conversation: Conversation, + processorRegistry: ProcessorRegistry + ): Promise { + const toolsInfoMap: ToolsInfoMap = {} + + for (const [attachmentName, _processor] of processorRegistry.entries()) { + const processor = _processor as ContextProcessor + + if (!processor.buildAgentTools) continue + + const toolConfigs = await processor.buildAgentTools( + conversation.attachments[attachmentName] as any, + conversation, + context + ) + + toolConfigs.forEach(toolConfig => { + const toolName = toolConfig.toolParams.name + toolsInfoMap[toolName] = { + attachmentName, + config: toolConfig, + tool: tool(toolConfig.toolCallback, toolConfig.toolParams) + } + }) + } + + return toolsInfoMap + } + + bindToolsToModel( + aiModel: AIModel, + aiTools: DynamicStructuredTool[], + aiModelAbortController: AbortController + ) { + return aiModel.bindTools!(aiTools).bind({ + signal: aiModelAbortController.signal + }) + } + + async invokeToolAndGetResult( + selectedTool: DynamicStructuredTool, + toolCall: ToolCall + ): Promise { + const toolMessage = await selectedTool.invoke(toolCall) + return ( + tryParseJSON(toolMessage?.content || toolMessage?.kwargs?.content) || {} + ) + } + + async processToolCalls( + toolCalls: ToolCall[], + aiToolsInfoMap: ToolsInfoMap, + lastConversation: Conversation, + context: ChatContext + ): Promise { + const updatedContext = { ...context } + const updatedLastConversation = { ...lastConversation } + + for (const toolCall of toolCalls) { + const toolInfo = aiToolsInfoMap[toolCall.name]! + const toolResult = await this.invokeToolAndGetResult( + toolInfo.tool, + toolCall + ) + + const newAttachment = await toolInfo.config.reBuildAttachment( + toolResult, + updatedLastConversation.attachments[toolInfo.attachmentName] as any, + updatedLastConversation, + updatedContext + ) + + updatedLastConversation.attachments = { + ...updatedLastConversation.attachments, + [toolInfo.attachmentName]: newAttachment + } + } + + updatedContext.conversations = [ + ...updatedContext.conversations.slice(0, -1), + updatedLastConversation + ] + + return updatedContext + } +} diff --git a/src/extension/webview-api/chat-context-processor/index.ts b/src/extension/webview-api/chat-context-processor/index.ts new file mode 100644 index 0000000..49908b7 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/index.ts @@ -0,0 +1,131 @@ +import { createModelProvider } from '@extension/ai/helpers' +import type { AIMessage } from '@langchain/core/messages' + +import { AttachmentProcessor } from './core/attachment-processor' +import { MessageBuilder } from './core/message-builder' +import { ProcessorRegistry } from './core/processor-registry' +import { ToolManager } from './core/tool-manager' +import { CodeProcessor } from './processors/code.processor' +import { CodebaseProcessor } from './processors/codebase.processor' +import { DocProcessor } from './processors/doc.processor' +import { FileProcessor } from './processors/file.processor' +import { GitProcessor } from './processors/git.processor' +import { WebProcessor } from './processors/web.processor' +import type { ChatContext } from './types/chat-context' +import type { Conversation } from './types/chat-context/conversation' +import type { AIModel } from './types/core' +import type { LangchainMessage } from './types/langchain-message' + +export class ChatContextProcessor { + private attachmentProcessor: AttachmentProcessor + + private toolManager: ToolManager + + constructor(private processorRegistry: ProcessorRegistry) { + this.attachmentProcessor = new AttachmentProcessor(processorRegistry) + this.toolManager = new ToolManager() + } + + async getAIMessageAnswer( + context: ChatContext, + allowTools: boolean + ): Promise { + const messages = await this.buildMessages(context) + const modelProvider = await createModelProvider() + const aiModelAbortController = new AbortController() + const aiModel = await modelProvider.getModel() + const lastConversation = + context.conversations[context.conversations.length - 1] + + if (lastConversation && allowTools) { + return this.processWithTools( + context, + lastConversation, + messages, + aiModel, + aiModelAbortController + ) + } + + return this.invokeModel(aiModel, messages, aiModelAbortController) + } + + private async processWithTools( + context: ChatContext, + lastConversation: Conversation, + messages: LangchainMessage[], + aiModel: AIModel, + aiModelAbortController: AbortController + ): Promise { + const aiToolsInfoMap = await this.toolManager.buildConversationToolsInfoMap( + context, + lastConversation, + this.processorRegistry + ) + const aiTools = Object.values(aiToolsInfoMap).map(({ tool }) => tool) + const aiModelWithTools = this.toolManager.bindToolsToModel( + aiModel, + aiTools, + aiModelAbortController + ) + const aiMessage = await aiModelWithTools.invoke(messages) + + if (!aiMessage.tool_calls?.length) { + return aiMessage + } + + const updatedContext = await this.toolManager.processToolCalls( + aiMessage.tool_calls, + aiToolsInfoMap, + lastConversation, + context + ) + return this.getAIMessageAnswer(updatedContext, false) + } + + private async invokeModel( + aiModel: AIModel, + messages: LangchainMessage[], + aiModelAbortController: AbortController + ): Promise { + return aiModel + .bind({ signal: aiModelAbortController.signal }) + .invoke(messages) + } + + async buildMessages(context: ChatContext): Promise { + return Promise.all( + context.conversations.map(conversation => + this.processConversation(conversation, context) + ) + ) + } + + private async processConversation( + conversation: Conversation, + context: ChatContext + ): Promise { + const processedContent = await this.attachmentProcessor.processAttachments( + conversation, + context + ) + const fullContent = processedContent + conversation.content + + return MessageBuilder.createMessage(conversation.type, { + content: fullContent + }) + } +} + +export const createChatContextProcessor = + async (): Promise => { + const registry = new ProcessorRegistry() + registry.register('fileContext', new FileProcessor()) + registry.register('codeContext', new CodeProcessor()) + registry.register('webContext', new WebProcessor()) + registry.register('docContext', new DocProcessor()) + registry.register('gitContext', new GitProcessor()) + registry.register('codebaseContext', new CodebaseProcessor()) + + return new ChatContextProcessor(registry) + } diff --git a/src/extension/webview-api/chat-context-processor/processors/code.processor.ts b/src/extension/webview-api/chat-context-processor/processors/code.processor.ts new file mode 100644 index 0000000..543383e --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/processors/code.processor.ts @@ -0,0 +1,51 @@ +import type { ChatContext } from '../types/chat-context' +import type { CodeContext } from '../types/chat-context/code-context' +import type { Conversation } from '../types/chat-context/conversation' +import type { ContextProcessor } from '../types/context-processor' +import type { LangchainMessageParams } from '../types/langchain-message' +import { formatCodeSnippet } from '../utils/code-snippet-formatter' + +export class CodeProcessor implements ContextProcessor { + async buildMessageParams( + attachment: CodeContext, + conversation: Conversation, + context: ChatContext + ): Promise { + const isLastConversation = + context.conversations.lastIndexOf(conversation) === + context.conversations.length - 1 + + return this.processCodeContext(attachment, isLastConversation) + } + + private processCodeContext( + codeContext: CodeContext, + isLastConversation: boolean + ): LangchainMessageParams { + let content = '' + + // Process codeChunks + for (const chunk of codeContext.codeChunks) { + content += formatCodeSnippet( + { + relativePath: chunk.relativePath, + code: chunk.code + }, + isLastConversation + ) + } + + // Process tmpCodeChunk + for (const tmpChunk of codeContext.tmpCodeChunk) { + content += formatCodeSnippet( + { + language: tmpChunk.language, + code: tmpChunk.code + }, + isLastConversation + ) + } + + return content + } +} diff --git a/src/extension/webview-api/chat-context-processor/processors/codebase.processor.ts b/src/extension/webview-api/chat-context-processor/processors/codebase.processor.ts new file mode 100644 index 0000000..290d7ab --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/processors/codebase.processor.ts @@ -0,0 +1,39 @@ +import type { ChatContext } from '../types/chat-context' +import type { CodebaseContext } from '../types/chat-context/codebase-context' +import type { Conversation } from '../types/chat-context/conversation' +import type { ContextProcessor } from '../types/context-processor' +import type { LangchainMessageParams } from '../types/langchain-message' +import { formatCodeSnippet } from '../utils/code-snippet-formatter' + +export class CodebaseProcessor implements ContextProcessor { + async buildMessageParams( + attachment: CodebaseContext, + conversation: Conversation, + context: ChatContext + ): Promise { + const isLastConversation = + context.conversations.lastIndexOf(conversation) === + context.conversations.length - 1 + + return this.processCodebaseContext(attachment, isLastConversation) + } + + private processCodebaseContext( + codebaseContext: CodebaseContext, + isLastConversation: boolean + ): LangchainMessageParams { + let content = 'Relevant codebase snippets:\n\n' + + for (const chunk of codebaseContext.relevantSnippets) { + content += formatCodeSnippet( + { + relativePath: chunk.relativePath, + code: chunk.code + }, + isLastConversation + ) + } + + return content + } +} diff --git a/src/extension/webview-api/chat-context-processor/processors/doc.processor.ts b/src/extension/webview-api/chat-context-processor/processors/doc.processor.ts new file mode 100644 index 0000000..d73a423 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/processors/doc.processor.ts @@ -0,0 +1,66 @@ +import { z } from 'zod' + +import { DocContext, type DocInfo } from '../types/chat-context/doc-context' +import type { ContextProcessor } from '../types/context-processor' +import type { LangchainMessageParams } from '../types/langchain-message' +import { createToolConfig, type ToolConfig } from '../types/langchain-tool' + +export class DocProcessor implements ContextProcessor { + async buildMessageParams( + attachment: DocContext + ): Promise { + return this.processDocContext(attachment) + } + + async buildAgentTools( + attachment: DocContext + ): Promise>> { + const { enableTool, allowSearchSiteUrls } = attachment + if (!enableTool || allowSearchSiteUrls?.length === 0) return [] + + const searchDocToolConfig = await createToolConfig({ + toolParams: { + name: 'searchDoc', + description: 'Search documentation', + schema: z.object({ + searchSiteUrl: z + .enum(allowSearchSiteUrls as [string, ...string[]]) + .describe('URL of the site to search documentation'), + keywords: z.string().describe('Keywords to search documentation') + }) + }, + toolCallback: async ({ searchSiteUrl, keywords }) => + this.searchDoc({ searchSiteUrl, keywords }), + reBuildAttachment: async (toolResult, attachment: DocContext) => ({ + ...attachment, + relevantDocs: toolResult ?? [] + }) + }) + + return [searchDocToolConfig] + } + + private async searchDoc({ + searchSiteUrl, + keywords + }: { + searchSiteUrl: string + keywords: string + }): Promise { + console.log('Searching documentation:', searchSiteUrl, keywords) + // TODO: Implement searchDoc + return [] + } + + private processDocContext(docContext: DocContext): LangchainMessageParams { + let content = 'Relevant documentation:\n\n' + + for (const doc of docContext.relevantDocs) { + content += `Title: ${doc.title}\n` + if (doc.url) content += `URL: ${doc.url}\n` + content += `Content:\n${doc.content}\n\n` + } + + return content + } +} diff --git a/src/extension/webview-api/chat-context-processor/processors/file.processor.ts b/src/extension/webview-api/chat-context-processor/processors/file.processor.ts new file mode 100644 index 0000000..eaba031 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/processors/file.processor.ts @@ -0,0 +1,71 @@ +import path from 'path' +import { traverseFileOrFolders } from '@extension/file-utils/traverse-fs' +import { VsCodeFS } from '@extension/file-utils/vscode-fs' +import { getLanguageId, getWorkspaceFolder } from '@extension/utils' +import type { MessageContentComplex } from '@langchain/core/messages' + +import type { FileContext, FileInfo } from '../types/chat-context/file-context' +import type { ContextProcessor } from '../types/context-processor' +import type { LangchainMessageParams } from '../types/langchain-message' + +export class FileProcessor implements ContextProcessor { + async buildMessageParams( + attachment: FileContext + ): Promise { + return await this.processFileContext(attachment) + } + + private async processFileContext( + fileContext: FileContext + ): Promise { + const workspacePath = getWorkspaceFolder().uri.fsPath + + const processFolder = async (folder: string): Promise => { + const files = await traverseFileOrFolders( + [folder], + workspacePath, + async (fileInfo: FileInfo) => { + const { relativePath, fullPath } = fileInfo + const languageId = getLanguageId(path.extname(relativePath).slice(1)) + const content = await VsCodeFS.readFileOrOpenDocumentContent(fullPath) + return `\`\`\`${languageId}:${relativePath}\n${content}\n\`\`\`\n\n` + } + ) + return files.join('') + } + + const processFile = async (file: FileInfo): Promise => { + const languageId = getLanguageId(path.extname(file.relativePath).slice(1)) + return `\`\`\`${languageId}:${file.relativePath}\n${file.content}\n\`\`\`\n\n` + } + + const [folderContents, fileContents] = await Promise.all([ + Promise.allSettled( + fileContext.selectedFolders.map(folder => + processFolder(folder.fullPath) + ) + ), + Promise.allSettled(fileContext.selectedFiles.map(processFile)) + ]) + + const messageParams = { + content: [ + { + type: 'text', + text: [...folderContents, ...fileContents].join('') + } + ] as MessageContentComplex[] + } satisfies LangchainMessageParams + + if (fileContext.selectedImages) { + messageParams.content.push( + ...fileContext.selectedImages.map(image => ({ + type: 'image_url', + image_url: image.url + })) + ) + } + + return messageParams + } +} diff --git a/src/extension/webview-api/chat-context-processor/processors/git.processor.ts b/src/extension/webview-api/chat-context-processor/processors/git.processor.ts new file mode 100644 index 0000000..4b572e4 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/processors/git.processor.ts @@ -0,0 +1,73 @@ +import { + GitCommit, + GitContext, + GitDiff, + type GitPullRequest +} from '../types/chat-context/git-context' +import type { ContextProcessor } from '../types/context-processor' +import type { LangchainMessageParams } from '../types/langchain-message' + +export class GitProcessor implements ContextProcessor { + async buildMessageParams( + attachment: GitContext + ): Promise { + return this.processGitContext(attachment) + } + + private processGitContext(gitContext: GitContext): LangchainMessageParams { + let content = '' + + content += this.processCommits(gitContext.commits) + content += this.processPullRequests(gitContext.pullRequests) + content += this.processDiffs(gitContext.diffs) + + return content + } + + private processCommits(commits: GitCommit[]): string { + if (commits.length === 0) return '' + + let content = 'Relevant commits:\n\n' + for (const commit of commits) { + content += `Commit: ${commit.sha}\n` + content += `Message: ${commit.message}\n` + content += `Author: ${commit.author}\n` + content += `Date: ${commit.date}\n` + content += 'Changes:\n' + content += this.processDiffs(commit.diff) + content += '\n' + } + return content + } + + private processPullRequests(prs: GitPullRequest[]): string { + if (prs.length === 0) return '' + + let content = 'Relevant pull requests:\n\n' + for (const pr of prs) { + content += `PR #${pr.id}: ${pr.title}\n` + content += `Description: ${pr.description}\n` + content += `Author: ${pr.author}\n` + content += `URL: ${pr.url}\n` + content += 'Changes:\n' + content += this.processDiffs(pr.diff) + content += '\n' + } + return content + } + + private processDiffs(diffs: GitDiff[]): string { + let content = '' + for (const diff of diffs) { + content += `File: ${diff.from} → ${diff.to}\n` + for (const chunk of diff.chunks) { + content += `${chunk.content}\n` + for (const line of chunk.lines) { + content += `${line}\n` + } + } + content += '\n' + } + return content + } +} diff --git a/src/extension/webview-api/chat-context-processor/processors/web.processor.ts b/src/extension/webview-api/chat-context-processor/processors/web.processor.ts new file mode 100644 index 0000000..1e43f30 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/processors/web.processor.ts @@ -0,0 +1,23 @@ +import type { WebContext } from '../types/chat-context/web-context' +import type { ContextProcessor } from '../types/context-processor' +import type { LangchainMessageParams } from '../types/langchain-message' + +export class WebProcessor implements ContextProcessor { + async buildMessageParams( + attachment: WebContext + ): Promise { + return this.processWebContext(attachment) + } + + private processWebContext(webContext: WebContext): LangchainMessageParams { + let content = 'Web search results:\n\n' + + for (const result of webContext.searchResults) { + content += `Title: ${result.title}\n` + content += `URL: ${result.url}\n` + content += `Summary: ${result.snippet}\n\n` + } + + return content + } +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/base-tool-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/base-tool-context.ts new file mode 100644 index 0000000..0f4f3be --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/base-tool-context.ts @@ -0,0 +1,3 @@ +export interface BaseToolContext { + enableTool: boolean +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/code-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/code-context.ts new file mode 100644 index 0000000..c74f4d0 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/code-context.ts @@ -0,0 +1,51 @@ +export interface CodeRange { + startPosition: { + /** + * @example 1 + */ + line: number + + /** + * @example 1 + */ + column: number + } + endPosition: { + /** + * @example 10 + */ + line: number + + /** + * @example 2 + */ + column: number + } +} + +export interface CodeChunk { + /** + * @example 'src/webview/types/vscode.d.ts' + */ + relativePath: string + + /** + * @example { startPosition: { line: 1, column: 1 }, endPosition: { line: 10, column: 10 } } + */ + range: CodeRange + + /** + * @example "import type { WebviewToExtensionsMsg } from '@shared/types'\n\ndeclare global {\n interface Window {\n acquireVsCodeApi(): {\n postMessage(msg: WebviewToExtensionsMsg): void\n setState(state: any): void\n getState(): any\n }\n vscode: ReturnType\n }\n}" + */ + code: string +} + +export interface TmpCodeChunk { + language: string + code: string +} + +export interface CodeContext { + tmpCodeChunk: TmpCodeChunk[] + codeChunks: CodeChunk[] +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/codebase-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/codebase-context.ts new file mode 100644 index 0000000..0eb326a --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/codebase-context.ts @@ -0,0 +1,12 @@ +export interface CodeSnippet { + relativePath: string + fullPath: string + startLine: number + endLine: number + code: string + relevance: number +} + +export interface CodebaseContext { + relevantSnippets: CodeSnippet[] +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/conversation.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/conversation.ts new file mode 100644 index 0000000..0aab786 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/conversation.ts @@ -0,0 +1,23 @@ +import type { MessageType } from '@langchain/core/messages' + +import type { CodeContext } from './code-context' +import type { CodebaseContext } from './codebase-context' +import type { DocContext } from './doc-context' +import type { FileContext } from './file-context' +import type { GitContext } from './git-context' +import type { WebContext } from './web-context' + +export interface Attachments { + codebaseContext: CodebaseContext + fileContext: FileContext + codeContext: CodeContext + webContext: WebContext + docContext: DocContext + gitContext: GitContext +} + +export interface Conversation { + type: MessageType + content: string + attachments: Attachments +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/doc-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/doc-context.ts new file mode 100644 index 0000000..423b270 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/doc-context.ts @@ -0,0 +1,12 @@ +import type { BaseToolContext } from './base-tool-context' + +export interface DocInfo { + title: string + content: string + url?: string +} + +export interface DocContext extends BaseToolContext { + allowSearchSiteUrls: string[] + relevantDocs: DocInfo[] +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/file-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/file-context.ts new file mode 100644 index 0000000..db3ce44 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/file-context.ts @@ -0,0 +1,34 @@ +/** + * Represents information about a file. + */ +export interface FileInfo { + /** + * The content of the file. + */ + content: string + + /** + * The relative path of the file. + */ + relativePath: string + + /** + * The full path of the file. + */ + fullPath: string +} + +export interface FolderInfo { + fullPath: string + relativePath: string +} + +export interface ImageInfo { + url: string +} + +export interface FileContext { + selectedFiles: FileInfo[] + selectedFolders: FolderInfo[] + selectedImages: ImageInfo[] +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/git-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/git-context.ts new file mode 100644 index 0000000..608c621 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/git-context.ts @@ -0,0 +1,61 @@ +export interface GitDiff { + /** + * @example '.github/workflows/ci.yml' + */ + from: string + + /** + * @example '.github/workflows/ci.yml' + */ + to: string + chunks: { + /** + * @example '@@ -1,6 +1,6 @@ importers:' + */ + content: string + + /** + * @example [ + * 'name: CI', + * 'on:', + * ' push:', + * '+ branches:', + * '+ - main', + * '- branches:', + * '- - master', + * ] + */ + lines: string[] + }[] +} + +export interface GitCommit { + /** + * @example '0bc7f06aa2930c2755c751615cfb2331de41ddb1' + */ + sha: string + + /** + * @example 'ci: fix ci' + */ + message: string + + diff: GitDiff[] + author: string + date: string +} + +export interface GitPullRequest { + id: number + title: string + description: string + author: string + url: string + diff: GitDiff[] +} + +export interface GitContext { + commits: GitCommit[] + pullRequests: GitPullRequest[] + diffs: GitDiff[] +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/index.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/index.ts new file mode 100644 index 0000000..9427dc2 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/index.ts @@ -0,0 +1,7 @@ +import type { Conversation } from './conversation' +import { SettingsContext } from './settings-context' + +export interface ChatContext { + conversations: Conversation[] + settings: SettingsContext +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/settings-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/settings-context.ts new file mode 100644 index 0000000..0b8db36 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/settings-context.ts @@ -0,0 +1,7 @@ +export interface SettingsContext { + modelName: string + useFastApply: boolean + fastApplyModelName?: string + explicitContext: string + allowLongFileScan: boolean +} diff --git a/src/extension/webview-api/chat-context-processor/types/chat-context/web-context.ts b/src/extension/webview-api/chat-context-processor/types/chat-context/web-context.ts new file mode 100644 index 0000000..55ea51e --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/chat-context/web-context.ts @@ -0,0 +1,9 @@ +export interface WebSearchResult { + url: string + title: string + snippet: string +} + +export interface WebContext { + searchResults: WebSearchResult[] +} diff --git a/src/extension/webview-api/chat-context-processor/types/context-processor.ts b/src/extension/webview-api/chat-context-processor/types/context-processor.ts new file mode 100644 index 0000000..427a503 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/context-processor.ts @@ -0,0 +1,25 @@ +import type { ChatContext } from './chat-context' +import type { BaseToolContext } from './chat-context/base-tool-context' +import type { Conversation } from './chat-context/conversation' +import type { LangchainMessageParams } from './langchain-message' +import type { ToolConfig } from './langchain-tool' + +interface BaseContextProcessor { + buildMessageParams( + attachment: Attachment, + conversation: Conversation, + context: ChatContext + ): Promise +} + +interface ToolContextProcessor { + buildAgentTools( + attachment: Attachment, + conversation: Conversation, + context: ChatContext + ): Promise[]> +} + +export type ContextProcessor = + BaseContextProcessor & + (Attachment extends BaseToolContext ? ToolContextProcessor : {}) diff --git a/src/extension/webview-api/chat-context-processor/types/core.ts b/src/extension/webview-api/chat-context-processor/types/core.ts new file mode 100644 index 0000000..3e18bdd --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/core.ts @@ -0,0 +1,20 @@ +import type { BaseFunctionCallOptions } from '@langchain/core/language_models/base' +import type { BaseChatModel } from '@langchain/core/language_models/chat_models' +import type { AIMessageChunk } from '@langchain/core/messages' +import type { DynamicStructuredTool } from '@langchain/core/tools' + +import type { BaseToolContext } from './chat-context/base-tool-context' +import type { Attachments } from './chat-context/conversation' +import type { ToolConfig } from './langchain-tool' + +export type ToolInfo = { + attachmentName: keyof Attachments + config: ToolConfig + tool: DynamicStructuredTool +} + +export type ToolName = string + +export type ToolsInfoMap = Record + +export type AIModel = BaseChatModel diff --git a/src/extension/webview-api/chat-context-processor/types/langchain-message.ts b/src/extension/webview-api/chat-context-processor/types/langchain-message.ts new file mode 100644 index 0000000..868b78a --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/langchain-message.ts @@ -0,0 +1,9 @@ +import { + AIMessage, + HumanMessage, + SystemMessage, + type BaseMessageFields +} from '@langchain/core/messages' + +export type LangchainMessage = HumanMessage | SystemMessage | AIMessage +export type LangchainMessageParams = string | BaseMessageFields diff --git a/src/extension/webview-api/chat-context-processor/types/langchain-tool.ts b/src/extension/webview-api/chat-context-processor/types/langchain-tool.ts new file mode 100644 index 0000000..2f91143 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/types/langchain-tool.ts @@ -0,0 +1,70 @@ +import type { ZodObjectAny } from '@langchain/core/dist/types/zod' +import type { RunnableFunc } from '@langchain/core/runnables' +import type { ResponseFormat, ToolParams } from '@langchain/core/tools' +import type { z } from 'zod' + +import type { ChatContext } from './chat-context' +import type { Conversation } from './chat-context/conversation' + +export interface ToolWrapperParams< + RunInput extends + | ZodObjectAny + | z.ZodString + | Record = ZodObjectAny +> extends ToolParams { + /** + * The name of the tool. If using with an LLM, this + * will be passed as the tool name. + */ + name: string + /** + * The description of the tool. + * @default `${fields.name} tool` + */ + description?: string + /** + * The input schema for the tool. If using an LLM, this + * will be passed as the tool schema to generate arguments + * for. + */ + schema?: RunInput + /** + * The tool response format. + * + * If "content" then the output of the tool is interpreted as the contents of a + * ToolMessage. If "content_and_artifact" then the output is expected to be a + * two-tuple corresponding to the (content, artifact) of a ToolMessage. + * + * @default "content" + */ + responseFormat?: ResponseFormat +} + +export type ToolConfig< + Attachment extends object, + ZodSchema extends ZodObjectAny = ZodObjectAny, + ToolReturnType = any +> = { + toolParams: ToolWrapperParams + toolCallback: RunnableFunc, ToolReturnType> + reBuildAttachment: ( + toolResult: ToolReturnType, + attachment: Attachment, + lastConversation: Conversation, + context: ChatContext + ) => Promise +} + +export function createToolConfig< + Attachment extends object, + ZodSchema extends ZodObjectAny, + ToolReturnType +>( + config: ToolConfig +): ToolConfig { + return { + toolParams: config.toolParams, + toolCallback: config.toolCallback, + reBuildAttachment: config.reBuildAttachment + } +} diff --git a/src/extension/webview-api/chat-context-processor/utils/code-snippet-formatter.ts b/src/extension/webview-api/chat-context-processor/utils/code-snippet-formatter.ts new file mode 100644 index 0000000..7285811 --- /dev/null +++ b/src/extension/webview-api/chat-context-processor/utils/code-snippet-formatter.ts @@ -0,0 +1,29 @@ +import path from 'path' +import { getLanguageId } from '@extension/utils' + +export interface CodeSnippetInfo { + relativePath?: string + language?: string + code: string +} + +export function formatCodeSnippet( + snippet: CodeSnippetInfo, + isEspeciallyRelevant: boolean +): string { + let codeSnippet: string + + if (snippet.relativePath) { + const languageId = getLanguageId( + path.extname(snippet.relativePath).slice(1) + ) + codeSnippet = `\`\`\`${languageId}:${snippet.relativePath}\n${snippet.code}\n\`\`\`\n\n` + } else { + codeSnippet = `\`\`\`${snippet.language || ''}\n${snippet.code}\n\`\`\`\n\n` + } + + if (isEspeciallyRelevant) { + return `\n${codeSnippet}\n` + } + return codeSnippet +} diff --git a/src/extension/webview-api/constant.ts b/src/extension/webview-api/constant.ts deleted file mode 100644 index de2875e..0000000 --- a/src/extension/webview-api/constant.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const REQUEST_TIMEOUT = 10 * 1000 -export const REQUEST_CLEANUP_THRESHOLD = 60 * 1000 -export const REQUEST_CLEANUP_INTERVAL = 30 * 1000 diff --git a/src/extension/webview-api/controllers/base.controller.ts b/src/extension/webview-api/controllers/base.controller.ts deleted file mode 100644 index 2b61489..0000000 --- a/src/extension/webview-api/controllers/base.controller.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { logger } from '@extension/logger' -import * as vscode from 'vscode' - -import { APIManager } from '../api-manager' -import { - APIError, - APIMethodMap, - Controller, - type ControllerHandlers, - type ControllerStreamHandlers -} from '../types' - -export abstract class BaseController implements Controller { - abstract name: string - - abstract handlers: ControllerHandlers - - streamHandlers?: ControllerStreamHandlers - - constructor( - protected context: vscode.ExtensionContext, - protected apiManager: APIManager - ) {} - - protected async safeCall< - C extends keyof APIMethodMap, - M extends keyof APIMethodMap[C] - >( - command: `${string & C}.${string & M}`, - sessionId: string, - params: APIMethodMap[C][M]['params'] - ): Promise { - try { - return await this.apiManager.callHandler(command, sessionId, params) - } catch (error) { - logger.warn(`Error calling ${command}:`, error) - - if (error instanceof APIError) { - throw error - } - throw new APIError( - 'INTERNAL_ERROR', - `Error calling ${command}`, - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error) - ) - } - } -} diff --git a/src/extension/webview-api/controllers/chat.controller.ts b/src/extension/webview-api/controllers/chat.controller.ts index 299b1b1..22294ea 100644 --- a/src/extension/webview-api/controllers/chat.controller.ts +++ b/src/extension/webview-api/controllers/chat.controller.ts @@ -1,57 +1,18 @@ -import { logger } from '@extension/logger' +import { Controller } from '../types' -import { APIError } from '../types' -import { BaseController } from './base.controller' +export class ChatController extends Controller { + readonly name = 'chat' -export const fetchChatStream = async ( - sessionId: string, - text: string -): Promise> => { - logger.log(text) - async function* mockStream() { - yield `[Session ${sessionId}] Hello` - yield `[Session ${sessionId}] How` - yield `[Session ${sessionId}] are` - yield `[Session ${sessionId}] you?` - } - return mockStream() -} - -export class ChatController extends BaseController { - name = 'chat' as const - - handlers = { - startChat: async (sessionId: string, params: { text: string }) => { - try { - const stream = await fetchChatStream(sessionId, params.text) - for await (const chunk of stream) { - this.apiManager.sendToWebview( - `${this.name}.streamUpdate`, - sessionId, - chunk - ) - } - // await this.safeCall('file.logChat', sessionId, { message: params.text }) - return 'Chat completed' - } catch (error) { - logger.warn(`Error in startChat for session ${sessionId}:`, error) - if (error instanceof APIError) { - throw error - } - throw new APIError( - 'CHAT_ERROR', - 'An error occurred during the chat', - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error) - ) - } - } + sendMessage(req: { message: string }): Promise { + return Promise.resolve(`Hi, bro, I'm response to: ${req.message}`) } - streamHandlers = { - streamUpdate: (sessionId: string, data: string) => { - logger.log(`Stream update for session ${sessionId}:`, data) + async *streamChat(req: { + prompt: string + }): AsyncGenerator { + for (let i = 0; i < 5; i++) { + yield `Chunk ${i + 1} for prompt: ${req.prompt}` + await new Promise(resolve => setTimeout(resolve, 1000)) } } } diff --git a/src/extension/webview-api/controllers/file.controller.ts b/src/extension/webview-api/controllers/file.controller.ts index 411dacc..94f5851 100644 --- a/src/extension/webview-api/controllers/file.controller.ts +++ b/src/extension/webview-api/controllers/file.controller.ts @@ -1,45 +1,47 @@ -import * as fs from 'fs/promises' -import { logger } from '@extension/logger' - -import { APIError } from '../types' -import { BaseController } from './base.controller' - -export class FileController extends BaseController { - name = 'file' as const - - handlers = { - readFile: async (sessionId: string, params: { path: string }) => { - try { - const content = await fs.readFile(params.path, 'utf-8') - return content - } catch (error) { - logger.warn(`Error reading file for session ${sessionId}:`, error) - throw new APIError( - 'FILE_READ_ERROR', - 'Failed to read file', - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error) - ) - } - }, - logChat: async (sessionId: string, params: { message: string }) => { - try { - await fs.appendFile( - 'chat.log', - `[Session ${sessionId}] ${params.message}\n` - ) - return 'Logged' - } catch (error) { - logger.warn(`Error logging chat for session ${sessionId}:`, error) - throw new APIError( - 'LOG_ERROR', - 'Failed to log chat', - error instanceof Error - ? { message: error.message, stack: error.stack } - : String(error) - ) - } - } +import { VsCodeFS } from '@extension/file-utils/vscode-fs' +import * as vscode from 'vscode' + +import { Controller } from '../types' + +export class FileController extends Controller { + readonly name = 'file' + + async readFile(req: { + path: string + encoding?: BufferEncoding + }): Promise { + return await VsCodeFS.readFileOrOpenDocumentContent(req.path, req.encoding) + } + + async writeFile(req: { + path: string + data: string + encoding?: BufferEncoding + }): Promise { + await VsCodeFS.writeFile(req.path, req.data, req.encoding) + } + + async mkdir(req: { path: string; recursive?: boolean }): Promise { + await VsCodeFS.mkdir(req.path, { recursive: req.recursive }) + } + + async rmdir(req: { path: string; recursive?: boolean }): Promise { + await VsCodeFS.rmdir(req.path, { recursive: req.recursive }) + } + + async unlink(req: { path: string }): Promise { + await VsCodeFS.unlink(req.path) + } + + async rename(req: { oldPath: string; newPath: string }): Promise { + await VsCodeFS.rename(req.oldPath, req.newPath) + } + + async stat(req: { path: string }): Promise { + return await VsCodeFS.stat(req.path) + } + + async readdir(req: { path: string }): Promise { + return await VsCodeFS.readdir(req.path) } } diff --git a/src/extension/webview-api/index.ts b/src/extension/webview-api/index.ts index 9995e76..ca9adc0 100644 --- a/src/extension/webview-api/index.ts +++ b/src/extension/webview-api/index.ts @@ -1,31 +1,98 @@ +import { getErrorMsg } from '@extension/utils' import * as vscode from 'vscode' -import { APIManager } from './api-manager' import { ChatController } from './controllers/chat.controller' -import { FileController } from './controllers/file.controller' -import type { APIMethodMap, WebviewPanel } from './types' +import type { + Controller, + ControllerClass, + ControllerMethod, + WebviewPanel +} from './types' -type Mutable = { -readonly [P in keyof T]: T[P] } +class APIManager { + private controllers: Map = new Map() -type InstanceTypeOfArray any>> = { - [K in keyof T]: T[K] extends new (...args: any) => infer R ? R : never -} + private webview: vscode.Webview | null = null + + constructor( + private context: vscode.ExtensionContext, + private panel: WebviewPanel, + controllerClasses: ControllerClass[] + ) { + this.webview = panel.webview + panel.webview.onDidReceiveMessage(this.handleMessage.bind(this)) + this.registerControllers(controllerClasses) + } + + private registerControllers(controllerClasses: ControllerClass[]) { + for (const ControllerClass of controllerClasses) { + const controller = new ControllerClass() + this.controllers.set(controller.name, controller) + } + } -const controllerConstructors = [ChatController, FileController] as const + private async handleMessage(message: any) { + const { id, controller: controllerName, method, data } = message + const controller = this.controllers.get(controllerName) -export type Controllers = Mutable< - InstanceTypeOfArray -> + if (!controller || !(method in controller)) { + this.sendError(id, `Method not found: ${controllerName}.${method}`) + return + } + + try { + const result = await (controller[method] as ControllerMethod)(data) + if (result && typeof result[Symbol.asyncIterator] === 'function') { + for await (const chunk of result as AsyncGenerator< + string, + void, + unknown + >) { + this.sendStream(id, chunk) + } + this.sendEnd(id) + } else { + this.sendResponse(id, result) + } + } catch (error) { + this.sendError(id, getErrorMsg(error)) + } + } + + private sendResponse(id: number, data: any) { + this.webview?.postMessage({ id, type: 'response', data }) + } + + private sendStream(id: number, data: string) { + this.webview?.postMessage({ id, type: 'stream', data }) + } + + private sendEnd(id: number) { + this.webview?.postMessage({ id, type: 'end' }) + } + + private sendError(id: number, error: string) { + this.webview?.postMessage({ id, type: 'error', error }) + } + + cleanUp() { + this.controllers.clear() + this.webview = null + } +} + +export const controllers = [ChatController] as const +export type Controllers = typeof controllers export const setupWebviewAPIManager = ( context: vscode.ExtensionContext, panel: WebviewPanel ): vscode.Disposable => { - const apiManager = new APIManager(context, panel) - - controllerConstructors.forEach(Controller => { - apiManager.registerController(Controller) - }) + const apiManager = new APIManager( + context, + panel, + controllers as any as ControllerClass[] + ) return { dispose: () => { diff --git a/src/extension/webview-api/prompts/chat.ts b/src/extension/webview-api/prompts/chat.ts deleted file mode 100644 index b85e0bc..0000000 --- a/src/extension/webview-api/prompts/chat.ts +++ /dev/null @@ -1,145 +0,0 @@ -export const chatWithCodebaseSystemPrompt = ` -You are an intelligent programmer, powered by GPT-4. You are happy to help answer any questions that the user has (usually they will be about coding). You will be given the context of the code in their file(s) and potentially relevant blocks of code. - -1. Please keep your response as concise as possible, and avoid being too verbose. - -2. Do not lie or make up facts. - -3. If a user messages you in a foreign language, please respond in that language. - -4. Format your response in markdown. - -5. When referencing code blocks in your answer, keep the following guidelines in mind: - - a. Never include line numbers in the output code. - - b. When outputting new code blocks, please specify the language ID after the initial backticks: -\`\`\`python -{{ code }} -\`\`\` - - c. When outputting code blocks for an existing file, include the file path after the initial backticks: -\`\`\`python:src/backend/main.py -{{ code }} -\`\`\` - - d. When referencing a code block the user gives you, only reference the start and end line numbers of the relevant code: -\`\`\`typescript:app/components/Todo.tsx -startLine: 2 -endLine: 30 -\`\`\` -` - -export const chatUserInstructionPrompt = ` -Please also follow these instructions in all of your responses if relevant to my query. No need to acknowledge these instructions directly in your response. - -总是说中文 - -` - -export const chatWithCodebaseFileContextPrompt = ` -# Inputs - -## Current File -Here is the file I'm looking at. It might be truncated from above and below and, if so, is centered around my cursor. -\`\`\`json:package.nls.en.json -当前文件内容 -\`\`\` - -## Potentially Relevant Code Snippets from the current Codebase -\`\`\`json:package.nls.zh-cn.json -相关文件内容 A -\`\`\` - - -\`\`\`json:package.json -相关文件内容 B -\`\`\` - - - - -------- - - - -------- - - - -` - -export const chatWithCodebaseUserPrompt = ` -优化一些翻译问题 - -If you need to reference any of the code blocks I gave you, only output the start and end line numbers. For example: -\`\`\`typescript:app/components/Todo.tsx -startLine: 200 -endLine: 310 -\`\`\` - -If you are writing code, do not include the "line_number|" before each line of code. -` - -export const chatWithFilesSystemPrompt = ` -You are an intelligent programmer, powered by GPT-4o. You are happy to help answer any questions that the user has (usually they will be about coding). - -1. Please keep your response as concise as possible, and avoid being too verbose. - -2. When the user is asking for edits to their code, please output a simplified version of the code block that highlights the changes necessary and adds comments to indicate where unchanged code has been skipped. For example: -\`\`\`file_path -// ... existing code ... -{{ edit_1 }} -// ... existing code ... -{{ edit_2 }} -// ... existing code ... -\`\`\` -The user can see the entire file, so they prefer to only read the updates to the code. Often this will mean that the start/end of the file will be skipped, but that's okay! Rewrite the entire file only if specifically requested. Always provide a brief explanation of the updates, unless the user specifically requests only the code. - -3. Do not lie or make up facts. - -4. If a user messages you in a foreign language, please respond in that language. - -5. Format your response in markdown. - -6. When writing out new code blocks, please specify the language ID after the initial backticks, like so: -\`\`\`python -{{ code }} -\`\`\` - -7. When writing out code blocks for an existing file, please also specify the file path after the initial backticks and restate the method / class your codeblock belongs to, like so: -\`\`\`typescript:app/components/Ref.tsx -function AIChatHistory() { - ... - {{ code }} - ... -} -\`\`\` -` - -export const chatWithFilesCursorFileContextPrompt = ` -# Inputs - -## Current File -Here is the file I'm looking at. It might be truncated from above and below and, if so, is centered around my cursor. -\`\`\`src/extension/auto-task/auto-task.ts -当前文件内容 -\`\`\` -` - -export const chatReplyAIPartPrompt = ` -# Inputs - -Please refer your answer to the following quote(s): -
-这里是 AI 的部分回复 -1. **代码格式化**:统一了代码的格式,使其更易读。 -
-` - -export const chatWithFilesSelectedFileContextPrompt = ` -\`\`\`typescript:src/extension/auto-task/types.ts -选中文件内容 -\`\`\` -优化这个屎山代码 @types.ts -` diff --git a/src/extension/webview-api/prompts/completions.ts b/src/extension/webview-api/prompts/completions.ts deleted file mode 100644 index 590fea5..0000000 --- a/src/extension/webview-api/prompts/completions.ts +++ /dev/null @@ -1,18 +0,0 @@ -// { -// "prompt": "// Path: src/extension/auto-task/utils.ts\nimport { MAX_RETRIES } from './constants'\n\nexport async function retryOperation(\n operation: () => Promise,\n maxRetries: number = MAX_RETRIES\n): Promise {\n for (let i = 0; i < maxRetries; i++) {\n try {\n return await operation()\n } catch (error) {\n if (i === maxRetries - 1) throw error\n await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** i))\n }\n }\n throw new Error('Max retries reached')\n}\n\nexport const retry = (operation: () => Promise) => retryOperation(operation)\n", -// "suffix": "", -// "max_tokens": 500, -// "temperature": 0.2, -// "top_p": 1, -// "n": 3, -// "stop": ["\n\n\n", "\n```"], -// "nwo": "nicepkg/aide", -// "stream": true, -// "extra": { -// "language": "typescript", -// "next_indent": 0, -// "trim_by_indentation": true, -// "prompt_tokens": 151, -// "suffix_tokens": 0 -// } -// } diff --git a/src/extension/webview-api/prompts/composer.ts b/src/extension/webview-api/prompts/composer.ts deleted file mode 100644 index dcd3934..0000000 --- a/src/extension/webview-api/prompts/composer.ts +++ /dev/null @@ -1,60 +0,0 @@ -export const composerContextSystemPrompt = ` -You are an intelligent programmer, powered by GPT-4o. You are happy to help answer any questions that the user has (usually they will be about coding). - -1. Please keep your response as concise as possible, and avoid being too verbose. - -2. When the user is asking for edits to their code, please output a simplified version of the code block that highlights the changes necessary and adds comments to indicate where unchanged code has been skipped. For example: -\`\`\`language:file_path -// ... existing code ... -{{ edit_1 }} -// ... existing code ... -{{ edit_2 }} -// ... existing code ... -\`\`\` -The user can see the entire file, so they prefer to only read the updates to the code. Often this will mean that the start/end of the file will be skipped, but that's okay! Rewrite the entire file only if specifically requested. Always provide a brief explanation of the updates, unless the user specifically requests only the code. -The current file is likely relevant to the edits, even if not specifically @ mentioned in the user's query. - -If you think that any of the imported files will likely need to change, please say so in your response. - -3. Do not lie or make up facts. - -4. If a user messages you in a foreign language, please respond in that language. - -5. Format your response in markdown. - -6. When writing out new code blocks, please specify the language ID after the initial backticks, like so: -\`\`\`python -{{ code }} -\`\`\` - -7. When writing out code blocks for an existing file, please also specify the file path after the initial backticks and restate the method / class the codeblock belongs to, like so: -\`\`\`typescript:app/components/Ref.tsx -function AIChatHistory() { - ... - {{ code }} - ... -} -\`\`\` - -8. For codeblocks used for explanation instead of suggestions, do not reference the file path. - -9. Put code into same codeblocks if they are the same file. - -10. Keep users' comments, unless user specifically requests to modify them. -` - -export const composerContextUserPrompt = ` - -\`\`\`typescript:src/webview/components/AutoTaskUI.tsx -文件 A 的代码 -\`\`\` - - - -\`\`\`typescript:src/webview/App.tsx -文件 B 的代码 -\`\`\` - - -@App.tsx 优化ui -` diff --git a/src/extension/webview-api/prompts/context.ts b/src/extension/webview-api/prompts/context.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/extension/webview-api/types.ts b/src/extension/webview-api/types.ts index 804d9ed..b726dce 100644 --- a/src/extension/webview-api/types.ts +++ b/src/extension/webview-api/types.ts @@ -2,40 +2,18 @@ import * as vscode from 'vscode' export type WebviewPanel = vscode.WebviewPanel | vscode.WebviewView -export type APIHandler = ( - this: Controller, - sessionId: string, - params: T -) => Promise +export type ControllerMethodResult = + | Promise + | AsyncGenerator -export type ControllerHandlers = Record -export type ControllerStreamHandlers = Record< - string, - (sessionId: string, data: any) => void -> -export interface Controller { - name: string - handlers: ControllerHandlers - streamHandlers?: ControllerStreamHandlers -} +export type ControllerMethod = ( + req: TReq +) => ControllerMethodResult -export type APIMethodMap = { - [controllerName: string]: { - [methodName: string]: { - params: any - result: any - stream?: any - } - } -} +export abstract class Controller { + abstract readonly name: string; -export class APIError extends Error { - constructor( - readonly code: string, - message: string, - readonly details?: any - ) { - super(message) - this.name = 'APIError' - } + [key: string]: ControllerMethod | string | undefined } + +export type ControllerClass = new () => Controller diff --git a/src/webview/App.tsx b/src/webview/App.tsx index a565e79..ef650a3 100644 --- a/src/webview/App.tsx +++ b/src/webview/App.tsx @@ -1,66 +1,10 @@ -import { VSCodeButton, VSCodeTextField } from '@vscode/webview-ui-toolkit/react' - -import { vscode } from './helpers/vscode' - -import './App.css' - -import { useState } from 'react' - -import { api } from './api/create-webview-api' +import { ChatEditor } from './components/chat-editor' +import { ChatProvider } from './contexts/chat-context' export default function App() { - const onPostMessage = () => { - vscode.postMessage({ - command: 'hello', - text: 'Hey there partner! 🤠' - }) - } - - const [message, setMessage] = useState('') - const [state, setState] = useState('') - - const onSetState = () => { - vscode.setState(state) - } - const onGetState = async () => { - console.log('state', await vscode.getState()) - setState((await vscode.getState()) as string) - - const msg = await api.sendMessage('chat.startChat', Date.now().toString(), { - text: 'Hello' - }) - - console.log('api get msg:', msg) - } - return ( -
-

Hello React!

- Test VSCode Message -
- setMessage(e?.target?.value)} - > - Please enter a message - -
Message is: {message}
-
-
- setState(e?.target?.value)} - > - Please enter a state - -
State is: {state}
-
- setState - - getState - -
-
-
+ + + ) } diff --git a/src/webview/App2.tsx b/src/webview/App2.tsx new file mode 100644 index 0000000..d53ef33 --- /dev/null +++ b/src/webview/App2.tsx @@ -0,0 +1,65 @@ +import { VSCodeButton, VSCodeTextField } from '@vscode/webview-ui-toolkit/react' + +import { vscode } from './utils/vscode' + +import './App.css' + +import { useState } from 'react' + +import { api } from './api/api-client' + +export default function App() { + const onPostMessage = () => { + vscode.postMessage({ + command: 'hello', + text: 'Hey there partner! 🤠' + }) + } + + const [message, setMessage] = useState('') + const [state, setState] = useState('') + + const onSetState = () => { + vscode.setState(state) + } + const onGetState = async () => { + console.log('state', await vscode.getState()) + setState((await vscode.getState()) as string) + + const msg = await api.chat.sendMessage?.({ message: 'Hello, AI!' }) + console.log(msg) + + console.log('api get msg:', msg) + } + + return ( +
+

Hello React!

+ Test VSCode Message +
+ setMessage(e?.target?.value)} + > + Please enter a message + +
Message is: {message}
+
+
+ setState(e?.target?.value)} + > + Please enter a state + +
State is: {state}
+
+ setState + + getState + +
+
+
+ ) +} diff --git a/src/webview/api/api-client/index.ts b/src/webview/api/api-client/index.ts new file mode 100644 index 0000000..0191c61 --- /dev/null +++ b/src/webview/api/api-client/index.ts @@ -0,0 +1,82 @@ +import type { Controllers } from '@extension/webview-api' +import type { Controller } from '@extension/webview-api/types' +import { vscode } from '@webview/utils/vscode' + +import type { ApiResponse, APIType } from './types' + +export class APIClient { + private messageId = 0 + + private pendingRequests: Map< + number, + { + resolve: (value: any) => void + reject: (reason: any) => void + onStream?: (chunk: string) => void + } + > = new Map() + + constructor() { + window.addEventListener('message', this.handleMessage.bind(this)) + } + + private handleMessage(event: MessageEvent) { + const message: ApiResponse & { id: number } = event.data + const pending = this.pendingRequests.get(message.id) + + if (!pending) return + + switch (message.type) { + case 'response': + pending.resolve(message.data) + this.pendingRequests.delete(message.id) + break + case 'stream': + pending.onStream?.(message.data) + break + case 'end': + pending.resolve(undefined) + this.pendingRequests.delete(message.id) + break + case 'error': + pending.reject(new Error(message.error)) + this.pendingRequests.delete(message.id) + break + default: + break + } + } + + async request( + controller: string, + method: string, + data: TReq, + onStream?: (chunk: string) => void + ): Promise { + return new Promise((resolve, reject) => { + const id = this.messageId++ + this.pendingRequests.set(id, { resolve, reject, onStream }) + vscode.postMessage({ id, controller, method, data }) + }) + } +} + +export const createWebviewApi = < + T extends readonly (new () => Controller)[] +>() => { + const apiClient = new APIClient() + return new Proxy({} as APIType, { + get: (target, controllerName: string) => + new Proxy( + {}, + { + get: + (_, method: string) => + (req: any, onStream?: (chunk: string) => void) => + apiClient.request(controllerName, method, req, onStream) + } + ) + }) as APIType +} + +export const api = createWebviewApi() diff --git a/src/webview/api/api-client/types.ts b/src/webview/api/api-client/types.ts new file mode 100644 index 0000000..b8c14e4 --- /dev/null +++ b/src/webview/api/api-client/types.ts @@ -0,0 +1,46 @@ +import type { Controller } from '@extension/webview-api/types' + +export type ApiResponse = + | { + type: 'response' + data: T + } + | { + type: 'stream' + data: string + } + | { + type: 'end' + } + | { + type: 'error' + error: string + } + +export type InferControllerMethods = T extends new (...args: any) => infer R + ? { + [K in keyof R as R[K] extends Function ? K : never]: R[K] + } + : never + +export type InferMethodParams = T[M] extends ( + req: infer P +) => any + ? P + : never +export type InferMethodReturn = T[M] extends ( + req: any +) => Promise + ? R + : T[M] extends (req: any) => AsyncGenerator + ? void + : never + +export type APIType Controller)[]> = { + [K in T[number] as InstanceType['name']]: { + [M in keyof InferControllerMethods]: ( + req: InferMethodParams, M>, + onStream?: (chunk: string) => void + ) => Promise, M>> + } +} diff --git a/src/webview/api/create-webview-api.ts b/src/webview/api/create-webview-api.ts deleted file mode 100644 index f33d4d1..0000000 --- a/src/webview/api/create-webview-api.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { Controllers } from '@extension/webview-api' -import type { - APIHandler, - Controller as ControllerType -} from '@extension/webview-api/types' -import { vscode } from '@webview/helpers/vscode' - -type ExtractMethodMap = T extends { name: infer N } - ? { - [K in N & string]: { - [M in keyof T['handlers']]: T['handlers'][M] extends APIHandler< - infer P, - infer R - > - ? { params: P; result: R } - : never - } - } - : never - -type CombineMethodMaps = ( - T extends (infer U)[] - ? U extends ControllerType - ? ExtractMethodMap - : never - : never -) extends infer O - ? { [K in keyof O]: O[K] } - : never - -type MergeMethodMaps = (T extends any ? (x: T) => void : never) extends ( - x: infer R -) => void - ? R - : never - -const createWebviewApi = () => { - type MethodMap = MergeMethodMaps> - - const callbacks: Record< - string, - { resolve: (value: any) => void; reject: (reason: any) => void } - > = {} - const streamHandlers: Record void> = - {} - - window.addEventListener('message', event => { - const { id, sessionId, command, result, error, data } = event.data - if (id) { - const callback = callbacks[id] - if (callback) { - if (error) { - callback.reject(new Error(JSON.stringify(error))) - } else { - callback.resolve(result) - } - delete callbacks[id] - } - } else if (command) { - const handler = streamHandlers[command] - if (handler) { - handler(sessionId, data) - } - } - }) - - const sendMessage = ( - command: `${string & C}.${string & M}`, - sessionId: string, - params: MethodMap[C][M] extends { params: infer P } ? P : never - ): Promise => - new Promise((resolve, reject) => { - const id = Math.random().toString(36).substring(2) - callbacks[id] = { resolve, reject } - vscode.postMessage({ command, sessionId, params, id }) - }) - - const onStream = ( - command: `${string & C}.${string & M}`, - handler: (sessionId: string, data: any) => void - ): void => { - streamHandlers[command] = handler - } - - return { sendMessage, onStream } -} - -export const api = createWebviewApi() -// api.sendMessage('chat.startChat', 'aaa', { -// text: 'Hello' -// }) diff --git a/src/webview/chat-context-manager/index.ts b/src/webview/chat-context-manager/index.ts deleted file mode 100644 index b6a4a72..0000000 --- a/src/webview/chat-context-manager/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ChatContext } from '@extension/webview-api/chat-context-builder/types/chat-context' - -import { BaseContextManager } from './managers/base.manager' -import { ConversationContextManager } from './managers/conversation.manager' -import { FileContextManager } from './managers/file.manager' -import { SettingsContextManager } from './managers/settings.manager' - -type ContextPart = Partial - -export class ChatContextManager< - T extends Record> -> { - private managers: T - - constructor(managers: T) { - this.managers = managers - } - - getContext(): ChatContext { - return Object.values(this.managers).reduce( - (acc, manager) => ({ - ...acc, - ...manager.getContext() - }), - {} as ChatContext - ) - } - - get(managerKey: K): T[K] { - return this.managers[managerKey] - } -} - -export const createChatContextManager = () => { - const chatContextManager = new ChatContextManager({ - file: new FileContextManager(), - conversation: new ConversationContextManager(), - settings: new SettingsContextManager() - }) - - // chatContextManager.get('file').addFile({ name: 'file1' }) - - return chatContextManager -} diff --git a/src/webview/chat-context-manager/managers/base.manager.ts b/src/webview/chat-context-manager/managers/base.manager.ts deleted file mode 100644 index d7134db..0000000 --- a/src/webview/chat-context-manager/managers/base.manager.ts +++ /dev/null @@ -1,15 +0,0 @@ -export abstract class BaseContextManager { - protected context: T - - constructor(initialContext: T) { - this.context = initialContext - } - - getContext(): T { - return JSON.parse(JSON.stringify(this.context)) - } - - updateContext(newContext: Partial): void { - this.context = { ...this.context, ...newContext } - } -} diff --git a/src/webview/chat-context-manager/managers/conversation.manager.ts b/src/webview/chat-context-manager/managers/conversation.manager.ts deleted file mode 100644 index f6d721e..0000000 --- a/src/webview/chat-context-manager/managers/conversation.manager.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { IConversationContext } from '@extension/webview-api/chat-context-builder/types/chat-context' -import type { Message } from '@extension/webview-api/chat-context-builder/types/chat-context/message' - -import { BaseContextManager } from './base.manager' - -export class ConversationContextManager extends BaseContextManager { - constructor() { - super({ - conversation: [], - references: { - selections: [], - fileSelections: [], - folderSelections: [], - useWeb: false, - useCodebase: false - }, - codeSelections: [], - plainText: '' - }) - } - - addMessage(message: Message): void { - if (!this.context.conversation) { - this.context.conversation = [] - } - this.context.conversation.push(message) - } - - removeMessage(index: number): void { - if ( - this.context.conversation && - index >= 0 && - index < this.context.conversation.length - ) { - this.context.conversation.splice(index, 1) - } - } - - getMessages(): Message[] { - return this.context.conversation || [] - } - - clearConversation(): void { - this.context.conversation = [] - } -} diff --git a/src/webview/chat-context-manager/managers/file.manager.ts b/src/webview/chat-context-manager/managers/file.manager.ts deleted file mode 100644 index 26380e8..0000000 --- a/src/webview/chat-context-manager/managers/file.manager.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IFileContext } from '@extension/webview-api/chat-context-builder/types/chat-context' - -import { BaseContextManager } from './base.manager' - -export class FileContextManager extends BaseContextManager { - constructor() { - super({ - focusedFiles: [], - suggestedFiles: [], - newlyCreatedFiles: [], - newlyCreatedFolders: [], - deleteFileSuggestions: [], - isReadingLongFile: false, - hasAddedFiles: false, - codeBlockData: {} - }) - } -} diff --git a/src/webview/chat-context-manager/managers/settings.manager.ts b/src/webview/chat-context-manager/managers/settings.manager.ts deleted file mode 100644 index 4c34b79..0000000 --- a/src/webview/chat-context-manager/managers/settings.manager.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ISettingsContext } from '@extension/webview-api/chat-context-builder/types/chat-context' - -import { BaseContextManager } from './base.manager' - -export class SettingsContextManager extends BaseContextManager { - constructor() { - super({ - modelName: '', - useFastApply: false, - useChunkSpeculationForLongFiles: false, - explicitContext: { context: '' }, - clickedCodeBlockContents: '', - allowLongFileScan: false - }) - } -} diff --git a/src/webview/components/chat-editor.tsx b/src/webview/components/chat-editor.tsx new file mode 100644 index 0000000..38f3256 --- /dev/null +++ b/src/webview/components/chat-editor.tsx @@ -0,0 +1,45 @@ +import { LexicalComposer } from '@lexical/react/LexicalComposer' +import { ContentEditable } from '@lexical/react/LexicalContentEditable' +import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary' +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import { MentionNode } from '@webview/lexical/nodes/mention-node' +import { MentionPlugin } from '@webview/lexical/plugins/mention-plugin' + +const theme = { + paragraph: 'editor-paragraph', + text: { + bold: 'editor-text-bold', + italic: 'editor-text-italic', + underline: 'editor-text-underline' + } +} + +function onError(error: Error) { + console.error('ChatEditor error:', error) +} + +export function ChatEditor() { + const initialConfig = { + namespace: 'MyEditor', + theme, + onError, + nodes: [MentionNode] + } + + return ( + +
+ } + placeholder={ +
Enter your message...
+ } + ErrorBoundary={LexicalErrorBoundary} + /> + + +
+
+ ) +} diff --git a/src/webview/components/mention-selector.tsx b/src/webview/components/mention-selector.tsx new file mode 100644 index 0000000..23d372e --- /dev/null +++ b/src/webview/components/mention-selector.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from 'react' +import type { FileInfo } from '@extension/file-utils/traverse-fs' +import type { GitCommit } from '@extension/webview-api/chat-context-processor/types/chat-context/git-context' +import { allMentionStrategies } from '@webview/lexical/mentions' + +interface MentionSelectorProps { + isOpen: boolean + onSelect: (type: string, data: any) => void + onClose: () => void +} + +export const MentionSelector: React.FC = ({ + isOpen, + onSelect, + onClose +}) => { + const [activeType, setActiveType] = useState(null) + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (isOpen && activeType) { + const strategy = allMentionStrategies.find(s => s.type === activeType) + if (strategy) { + setLoading(true) + setError(null) + strategy + .getData() + .then(data => { + setItems(data) + setLoading(false) + }) + .catch(err => { + console.error('Error fetching data:', err) + setError('Failed to load data') + setLoading(false) + }) + } + } + }, [isOpen, activeType]) + + if (!isOpen) return null + + return ( +
+
+ {allMentionStrategies.map(strategy => ( + + ))} +
+
+ {loading &&

Loading...

} + {error &&

{error}

} + {!loading && + !error && + items.map((item, index) => ( +
{ + onSelect(activeType!, item) + onClose() + }} + > + {/* Render item based on its type */} + {activeType === 'file' && (item as FileInfo).relativePath} + {activeType === 'gitCommit' && (item as GitCommit).message} + {/* Add more type-specific rendering logic here */} +
+ ))} +
+
+ ) +} diff --git a/src/webview/contexts/chat-context.tsx b/src/webview/contexts/chat-context.tsx new file mode 100644 index 0000000..8f4a528 --- /dev/null +++ b/src/webview/contexts/chat-context.tsx @@ -0,0 +1,51 @@ +import React, { createContext, useCallback, useContext, useState } from 'react' +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import { v4 as uuidv4 } from 'uuid' + +import type { IChatContext, IConversation } from '../types/chat' + +const ChatContext = createContext(undefined) + +export const ChatProvider: React.FC<{ + children: React.ReactNode +}> = ({ children }) => { + const [conversations, setConversations] = useState([]) + const [currentAttachments, setCurrentAttachments] = useState( + {} as Attachments + ) + + const addConversation = useCallback((conversation: IConversation) => { + setConversations(prev => [...prev, { ...conversation, id: uuidv4() }]) + setCurrentAttachments({} as Attachments) + }, []) + + const updateCurrentAttachments = useCallback( + (newAttachments: Partial) => { + setCurrentAttachments(prev => ({ ...prev, ...newAttachments })) + }, + [] + ) + + const resetChat = useCallback(() => { + setConversations([]) + setCurrentAttachments({} as Attachments) + }, []) + + const value: IChatContext = { + conversations, + currentAttachments, + addConversation, + updateCurrentAttachments, + resetChat + } + + return {children} +} + +export const useChatContext = () => { + const context = useContext(ChatContext) + if (context === undefined) { + throw new Error('useChatContext must be used within a ChatProvider') + } + return context +} diff --git a/src/webview/hooks/use-mention-context.ts b/src/webview/hooks/use-mention-context.ts new file mode 100644 index 0000000..3e940e7 --- /dev/null +++ b/src/webview/hooks/use-mention-context.ts @@ -0,0 +1,32 @@ +import { useCallback, useState } from 'react' +import { useChatContext } from '@webview/contexts/chat-context' + +import { allMentionStrategies } from '../lexical/mentions' + +export function useMentionContext() { + const { currentAttachments, updateCurrentAttachments } = useChatContext() + const [activeMentionType, setActiveMentionType] = useState( + null + ) + + const handleMentionSelect = useCallback( + (type: string, data: any) => { + const strategy = allMentionStrategies.find(s => s.type === type) + if (strategy) { + const updatedAttachments = strategy.updateAttachments( + data, + currentAttachments + ) + updateCurrentAttachments(updatedAttachments) + } + setActiveMentionType(null) + }, + [currentAttachments, updateCurrentAttachments] + ) + + return { + activeMentionType, + setActiveMentionType, + handleMentionSelect + } +} diff --git a/src/webview/lexical/mentions/code-chunk-mention-strategy.ts b/src/webview/lexical/mentions/code-chunk-mention-strategy.ts new file mode 100644 index 0000000..3334f1b --- /dev/null +++ b/src/webview/lexical/mentions/code-chunk-mention-strategy.ts @@ -0,0 +1,27 @@ +import type { CodeChunk } from '@extension/webview-api/chat-context-processor/types/chat-context/code-context' +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { IMentionStrategy } from '@webview/types/chat' + +export class CodeChunkMentionStrategy implements IMentionStrategy { + type = 'codeChunk' + + async getData(): Promise { + // 实现从 VSCode 扩展获取代码块的逻辑 + return [] + } + + updateAttachments( + data: CodeChunk, + currentAttachments: Attachments + ): Partial { + return { + codeContext: { + ...currentAttachments.codeContext, + codeChunks: [ + ...(currentAttachments.codeContext?.codeChunks || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/mentions/code-snippet-mention-strategy.ts b/src/webview/lexical/mentions/code-snippet-mention-strategy.ts new file mode 100644 index 0000000..ebbde2e --- /dev/null +++ b/src/webview/lexical/mentions/code-snippet-mention-strategy.ts @@ -0,0 +1,27 @@ +import type { CodeSnippet } from '@extension/webview-api/chat-context-processor/types/chat-context/codebase-context' +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { IMentionStrategy } from '@webview/types/chat' + +export class CodeSnippetMentionStrategy implements IMentionStrategy { + type = 'codeSnippet' + + async getData(): Promise { + // 实现从 VSCode 扩展获取代码片段的逻辑 + return [] + } + + updateAttachments( + data: CodeSnippet, + currentAttachments: Attachments + ): Partial { + return { + codebaseContext: { + ...currentAttachments.codebaseContext, + relevantSnippets: [ + ...(currentAttachments.codebaseContext?.relevantSnippets || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/mentions/doc-mention-strategy.ts b/src/webview/lexical/mentions/doc-mention-strategy.ts new file mode 100644 index 0000000..5dfe145 --- /dev/null +++ b/src/webview/lexical/mentions/doc-mention-strategy.ts @@ -0,0 +1,27 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { DocInfo } from '@extension/webview-api/chat-context-processor/types/chat-context/doc-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class DocMentionStrategy implements IMentionStrategy { + type = 'doc' + + async getData(): Promise { + // 实现从 VSCode 扩展获取文档信息的逻辑 + return [] + } + + updateAttachments( + data: DocInfo, + currentAttachments: Attachments + ): Partial { + return { + docContext: { + ...currentAttachments.docContext, + relevantDocs: [ + ...(currentAttachments.docContext?.relevantDocs || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/mentions/file-mention-strategy.ts b/src/webview/lexical/mentions/file-mention-strategy.ts new file mode 100644 index 0000000..9dc7550 --- /dev/null +++ b/src/webview/lexical/mentions/file-mention-strategy.ts @@ -0,0 +1,28 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { FileInfo } from '@extension/webview-api/chat-context-processor/types/chat-context/file-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class FileMentionStrategy implements IMentionStrategy { + type = 'file' + + async getData(): Promise { + // 实现从 VSCode 扩展获取文件列表的逻辑 + // 这里需要与 VSCode 扩展通信 + return [] + } + + updateAttachments( + data: FileInfo, + currentAttachments: Attachments + ): Partial { + return { + fileContext: { + ...currentAttachments.fileContext, + selectedFiles: [ + ...(currentAttachments.fileContext?.selectedFiles || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/mentions/folder-mention-strategy.ts b/src/webview/lexical/mentions/folder-mention-strategy.ts new file mode 100644 index 0000000..5f905c6 --- /dev/null +++ b/src/webview/lexical/mentions/folder-mention-strategy.ts @@ -0,0 +1,27 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { FolderInfo } from '@extension/webview-api/chat-context-processor/types/chat-context/file-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class FolderMentionStrategy implements IMentionStrategy { + type = 'folder' + + async getData(): Promise { + // 实现从 VSCode 扩展获取文件夹列表的逻辑 + return [] + } + + updateAttachments( + data: FolderInfo, + currentAttachments: Attachments + ): Partial { + return { + fileContext: { + ...currentAttachments.fileContext, + selectedFolders: [ + ...(currentAttachments.fileContext?.selectedFolders || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/mentions/git-commit-mention-strategy.ts b/src/webview/lexical/mentions/git-commit-mention-strategy.ts new file mode 100644 index 0000000..c876da5 --- /dev/null +++ b/src/webview/lexical/mentions/git-commit-mention-strategy.ts @@ -0,0 +1,24 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { GitCommit } from '@extension/webview-api/chat-context-processor/types/chat-context/git-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class GitCommitMentionStrategy implements IMentionStrategy { + type = 'gitCommit' + + async getData(): Promise { + // 实现从 VSCode 扩展获取 Git 提交信息的逻辑 + return [] + } + + updateAttachments( + data: GitCommit, + currentAttachments: Attachments + ): Partial { + return { + gitContext: { + ...currentAttachments.gitContext, + commits: [...(currentAttachments.gitContext?.commits || []), data] + } + } + } +} diff --git a/src/webview/lexical/mentions/git-diff-mention-strategy.ts b/src/webview/lexical/mentions/git-diff-mention-strategy.ts new file mode 100644 index 0000000..5c18c90 --- /dev/null +++ b/src/webview/lexical/mentions/git-diff-mention-strategy.ts @@ -0,0 +1,24 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { GitDiff } from '@extension/webview-api/chat-context-processor/types/chat-context/git-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class GitDiffMentionStrategy implements IMentionStrategy { + type = 'gitDiff' + + async getData(): Promise { + // 实现从 VSCode 扩展获取 Git Diff 信息的逻辑 + return [] + } + + updateAttachments( + data: GitDiff, + currentAttachments: Attachments + ): Partial { + return { + gitContext: { + ...currentAttachments.gitContext, + diffs: [...(currentAttachments.gitContext?.diffs || []), data] + } + } + } +} diff --git a/src/webview/lexical/mentions/git-pull-request-mention-strategy.ts b/src/webview/lexical/mentions/git-pull-request-mention-strategy.ts new file mode 100644 index 0000000..eff8fd9 --- /dev/null +++ b/src/webview/lexical/mentions/git-pull-request-mention-strategy.ts @@ -0,0 +1,27 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { GitPullRequest } from '@extension/webview-api/chat-context-processor/types/chat-context/git-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class GitPullRequestMentionStrategy implements IMentionStrategy { + type = 'gitPullRequest' + + async getData(): Promise { + // 实现从 VSCode 扩展获取 Git Pull Request 信息的逻辑 + return [] + } + + updateAttachments( + data: GitPullRequest, + currentAttachments: Attachments + ): Partial { + return { + gitContext: { + ...currentAttachments.gitContext, + pullRequests: [ + ...(currentAttachments.gitContext?.pullRequests || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/mentions/image-mention-strategy.ts b/src/webview/lexical/mentions/image-mention-strategy.ts new file mode 100644 index 0000000..ae4e0fd --- /dev/null +++ b/src/webview/lexical/mentions/image-mention-strategy.ts @@ -0,0 +1,27 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { ImageInfo } from '@extension/webview-api/chat-context-processor/types/chat-context/file-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class ImageMentionStrategy implements IMentionStrategy { + type = 'image' + + async getData(): Promise { + // 实现从 VSCode 扩展获取图片列表的逻辑 + return [] + } + + updateAttachments( + data: ImageInfo, + currentAttachments: Attachments + ): Partial { + return { + fileContext: { + ...currentAttachments.fileContext, + selectedImages: [ + ...(currentAttachments.fileContext?.selectedImages || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/mentions/index.ts b/src/webview/lexical/mentions/index.ts new file mode 100644 index 0000000..14e3e88 --- /dev/null +++ b/src/webview/lexical/mentions/index.ts @@ -0,0 +1,25 @@ +import type { IMentionStrategy } from '@webview/types/chat' + +import { CodeChunkMentionStrategy } from './code-chunk-mention-strategy' +import { CodeSnippetMentionStrategy } from './code-snippet-mention-strategy' +import { DocMentionStrategy } from './doc-mention-strategy' +import { FileMentionStrategy } from './file-mention-strategy' +import { FolderMentionStrategy } from './folder-mention-strategy' +import { GitCommitMentionStrategy } from './git-commit-mention-strategy' +import { GitDiffMentionStrategy } from './git-diff-mention-strategy' +import { GitPullRequestMentionStrategy } from './git-pull-request-mention-strategy' +import { ImageMentionStrategy } from './image-mention-strategy' +import { WebSearchMentionStrategy } from './web-search-mention-strategy' + +export const allMentionStrategies: IMentionStrategy[] = [ + new FileMentionStrategy(), + new FolderMentionStrategy(), + new ImageMentionStrategy(), + new CodeChunkMentionStrategy(), + new CodeSnippetMentionStrategy(), + new DocMentionStrategy(), + new WebSearchMentionStrategy(), + new GitCommitMentionStrategy(), + new GitPullRequestMentionStrategy(), + new GitDiffMentionStrategy() +] diff --git a/src/webview/lexical/mentions/web-search-mention-strategy.ts b/src/webview/lexical/mentions/web-search-mention-strategy.ts new file mode 100644 index 0000000..72a8273 --- /dev/null +++ b/src/webview/lexical/mentions/web-search-mention-strategy.ts @@ -0,0 +1,27 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { WebSearchResult } from '@extension/webview-api/chat-context-processor/types/chat-context/web-context' +import type { IMentionStrategy } from '@webview/types/chat' + +export class WebSearchMentionStrategy implements IMentionStrategy { + type = 'webSearch' + + async getData(): Promise { + // 实现 Web 搜索的逻辑 + return [] + } + + updateAttachments( + data: WebSearchResult, + currentAttachments: Attachments + ): Partial { + return { + webContext: { + ...currentAttachments.webContext, + searchResults: [ + ...(currentAttachments.webContext?.searchResults || []), + data + ] + } + } + } +} diff --git a/src/webview/lexical/nodes/mention-node.ts b/src/webview/lexical/nodes/mention-node.ts new file mode 100644 index 0000000..c66f4f1 --- /dev/null +++ b/src/webview/lexical/nodes/mention-node.ts @@ -0,0 +1,95 @@ +import { EditorConfig, NodeKey, SerializedTextNode, TextNode } from 'lexical' + +export type SerializedMentionNode = SerializedTextNode & { + mentionType: string + mentionData: any +} + +export class MentionNode extends TextNode { + __mentionType: string + + __mentionData: any + + static getType(): string { + return 'mention' + } + + static clone(node: MentionNode): MentionNode { + return new MentionNode( + node.__mentionType, + node.__mentionData, + node.__text, + node.__key + ) + } + + constructor( + mentionType: string, + mentionData: any, + text?: string, + key?: NodeKey + ) { + super(text || `@${mentionType}`, key) + this.__mentionType = mentionType + this.__mentionData = mentionData + } + + createDOM(config: EditorConfig): HTMLElement { + const dom = super.createDOM(config) + dom.style.cssText = + 'background-color: #eee; border-radius: 3px; padding: 1px 3px;' + dom.className = 'mention' + return dom + } + + updateDOM( + prevNode: MentionNode, + dom: HTMLElement, + config: EditorConfig + ): boolean { + const isUpdated = super.updateDOM(prevNode, dom, config) + return isUpdated + } + + static importJSON(serializedNode: SerializedMentionNode): MentionNode { + const node = $createMentionNode( + serializedNode.mentionType, + serializedNode.mentionData + ) + node.setTextContent(serializedNode.text) + node.setFormat(serializedNode.format) + node.setDetail(serializedNode.detail) + node.setMode(serializedNode.mode) + node.setStyle(serializedNode.style) + return node + } + + exportJSON(): SerializedMentionNode { + return { + ...super.exportJSON(), + mentionType: this.__mentionType, + mentionData: this.__mentionData, + type: 'mention', + version: 1 + } + } + + getMentionType(): string { + return this.__mentionType + } + + getMentionData(): any { + return this.__mentionData + } +} + +export function $createMentionNode( + mentionType: string, + mentionData: any +): MentionNode { + return new MentionNode(mentionType, mentionData) +} + +export function $isMentionNode(node: any): node is MentionNode { + return node instanceof MentionNode +} diff --git a/src/webview/lexical/plugins/mention-plugin.tsx b/src/webview/lexical/plugins/mention-plugin.tsx new file mode 100644 index 0000000..0bc4714 --- /dev/null +++ b/src/webview/lexical/plugins/mention-plugin.tsx @@ -0,0 +1,49 @@ +import { useEffect } from 'react' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { MentionSelector } from '@webview/components/mention-selector' +import { useMentionContext } from '@webview/hooks/use-mention-context' +import { $getSelection, $isRangeSelection } from 'lexical' + +import { $createMentionNode } from '../nodes/mention-node' + +export function MentionPlugin(): JSX.Element { + const [editor] = useLexicalComposerContext() + const { activeMentionType, setActiveMentionType, handleMentionSelect } = + useMentionContext() + + useEffect(() => { + const removeListener = editor.registerTextContentListener( + (textContent: string) => { + const match = textContent.match(/@(\w*)$/) + if (match) { + setActiveMentionType('') + } else { + setActiveMentionType(null) + } + } + ) + + return () => { + removeListener() + } + }, [editor, setActiveMentionType]) + + const onMentionSelect = (type: string, data: any) => { + editor.update(() => { + const selection = $getSelection() + if ($isRangeSelection(selection)) { + const mentionNode = $createMentionNode(type, data) + selection.insertNodes([mentionNode]) + } + }) + handleMentionSelect(type, data) + } + + return ( + setActiveMentionType(null)} + /> + ) +} diff --git a/src/webview/types/chat.ts b/src/webview/types/chat.ts new file mode 100644 index 0000000..9484742 --- /dev/null +++ b/src/webview/types/chat.ts @@ -0,0 +1,26 @@ +import type { Attachments } from '@extension/webview-api/chat-context-processor/types/chat-context/conversation' +import type { MessageType } from '@langchain/core/messages' + +export interface IConversation { + id: string + type: MessageType + content: string + attachments: Attachments +} + +export interface IChatContext { + conversations: IConversation[] + currentAttachments: Attachments + addConversation: (conversation: IConversation) => void + updateCurrentAttachments: (newAttachments: Partial) => void + resetChat: () => void +} + +export interface IMentionStrategy { + type: string + getData: () => Promise + updateAttachments: ( + data: any, + currentAttachments: Attachments + ) => Partial +} diff --git a/src/webview/helpers/vscode.ts b/src/webview/utils/vscode.ts similarity index 100% rename from src/webview/helpers/vscode.ts rename to src/webview/utils/vscode.ts