diff --git a/package-lock.json b/package-lock.json index ba62493..eb6b82f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "devchat", - "version": "0.1.22", + "version": "0.1.33", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "devchat", - "version": "0.1.22", + "version": "0.1.33", "dependencies": { "@emotion/react": "^11.10.8", "@mantine/core": "^6.0.10", @@ -31,7 +31,6 @@ "ncp": "^2.0.0", "node-fetch": "^3.3.1", "nonce": "^1.0.4", - "openai": "^3.2.1", "quote": "^0.4.0", "react-markdown": "^8.0.7", "react-syntax-highlighter": "^15.5.0", @@ -41,6 +40,7 @@ "unified": "^11.0.3", "unist-util-visit": "^5.0.0", "uuid": "^9.0.0", + "xmlrpc": "^1.3.2", "yaml": "^2.3.2" }, "devDependencies": { @@ -90,7 +90,8 @@ "vscode-test": "^1.6.1", "webpack": "^5.76.3", "webpack-cli": "^5.0.1", - "webpack-dev-server": "^4.13.3" + "webpack-dev-server": "^4.13.3", + "xmlrpc": "^1.3.2" }, "engines": { "vscode": "^1.75.0" @@ -109,16 +110,81 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", + "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.21.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz", @@ -170,11 +236,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", - "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", + "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", "dependencies": { - "@babel/types": "^7.21.5", + "@babel/types": "^7.23.4", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -331,31 +397,31 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", - "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -481,28 +547,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -544,12 +610,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -621,9 +687,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", - "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", + "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1925,31 +1991,31 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", - "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", + "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.5", - "@babel/types": "^7.21.5", + "@babel/code-frame": "^7.23.4", + "@babel/generator": "^7.23.4", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.4", + "@babel/types": "^7.23.4", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1966,12 +2032,12 @@ } }, "node_modules/@babel/types": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", - "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", + "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", "dependencies": { - "@babel/helper-string-parser": "^7.21.5", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -4775,9 +4841,9 @@ } }, "node_modules/axios": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", - "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -7264,9 +7330,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -11259,23 +11325,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openai": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", - "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", - "dependencies": { - "axios": "^0.26.0", - "form-data": "^4.0.0" - } - }, - "node_modules/openai/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -11593,9 +11642,9 @@ } }, "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -12955,6 +13004,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -14952,6 +15007,29 @@ } } }, + "node_modules/xmlbuilder": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlrpc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", + "integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==", + "dev": true, + "dependencies": { + "sax": "1.2.x", + "xmlbuilder": "8.2.x" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -15067,11 +15145,63 @@ } }, "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", + "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { @@ -15114,11 +15244,11 @@ } }, "@babel/generator": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", - "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", + "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", "requires": { - "@babel/types": "^7.21.5", + "@babel/types": "^7.23.4", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -15241,25 +15371,25 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", - "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -15352,22 +15482,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==" + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.21.0", @@ -15397,12 +15527,12 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -15458,9 +15588,9 @@ } }, "@babel/parser": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", - "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==" + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", + "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -16331,28 +16461,28 @@ } }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", - "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", + "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", "requires": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.5", - "@babel/types": "^7.21.5", + "@babel/code-frame": "^7.23.4", + "@babel/generator": "^7.23.4", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.4", + "@babel/types": "^7.23.4", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -16365,12 +16495,12 @@ } }, "@babel/types": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", - "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", + "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", "requires": { - "@babel/helper-string-parser": "^7.21.5", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -18554,9 +18684,9 @@ "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" }, "axios": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz", - "integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -20403,9 +20533,9 @@ "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { @@ -23136,25 +23266,6 @@ "is-wsl": "^2.2.0" } }, - "openai": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", - "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", - "requires": { - "axios": "^0.26.0", - "form-data": "^4.0.0" - }, - "dependencies": { - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "requires": { - "follow-redirects": "^1.14.8" - } - } - } - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -23399,9 +23510,9 @@ } }, "postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", @@ -24418,6 +24529,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, "scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", diff --git a/package.json b/package.json index 9f8b5a9..f88d0a6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "devchat", "displayName": "DevChat", "description": "Write prompts, not code", - "version": "0.1.33", + "version": "0.1.55", "icon": "assets/devchat.png", "publisher": "merico", "engines": { @@ -630,7 +630,7 @@ "title": "DevChat" }, { - "command": "devchat.addConext", + "command": "devchat.addContext", "title": "Add to DevChat" }, { @@ -727,7 +727,7 @@ "when": "false" }, { - "command": "devchat.addConext", + "command": "devchat.addContext", "when": "false" }, { @@ -759,7 +759,7 @@ }, { "when": "!isChineseLocale && resourceLangId != 'git'", - "command": "devchat.addConext", + "command": "devchat.addContext", "group": "navigation" } ], @@ -874,7 +874,6 @@ "ncp": "^2.0.0", "node-fetch": "^3.3.1", "nonce": "^1.0.4", - "openai": "^3.2.1", "quote": "^0.4.0", "react-markdown": "^8.0.7", "react-syntax-highlighter": "^15.5.0", @@ -886,4 +885,4 @@ "uuid": "^9.0.0", "yaml": "^2.3.2" } -} \ No newline at end of file +} diff --git a/src/action/actionManager.ts b/src/action/actionManager.ts deleted file mode 100644 index 0961eeb..0000000 --- a/src/action/actionManager.ts +++ /dev/null @@ -1,211 +0,0 @@ -import fs from 'fs'; - -import { Action, CustomActions, getActionInstruction } from './customAction'; - -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; - -import { SymbolRefAction } from './symbolRefAction'; -import { SymbolDefAction } from './symbolDefAction'; -import { AskInputAction } from './askInputAction'; -import { SymbolInFileAction } from './symbolInFileAction'; -import { CurrentDocumentAction } from './currentDocumentAction'; -import { SelectTextAction, SelectBlockAction } from './selectContextAction'; -import { RunVSCodeCommandAction } from './runVSCodeCommand'; - - -// extend Action -export class CommandRunAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "from": string }[]; - - constructor() { - this.name = 'command_run'; - this.description = 'run command'; - this.type = ['command']; - this.action = 'command_run'; - this.handler = []; - this.args = [ - {"name": "content", "description": "command json to run", "type": "string", "from": "content.content"}, - ]; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const commandData = JSON.parse(args.content); - const result = await ActionManager.getInstance().applyCommandAction(commandData.name, commandData.arguments); - return result; - } catch (error) { - logger.channel()?.error('Failed to parse code file content: ' + error); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `Failed to parse code file content: ${error}`}; - } - } -}; - -export default class ActionManager { - private static instance: ActionManager; - private actions: Action[] = []; - - private constructor() { } - - public static getInstance(): ActionManager { - if (!ActionManager.instance) { - ActionManager.instance = new ActionManager(); - - ActionManager.instance.registerAction(new CommandRunAction()); - ActionManager.instance.registerAction(new SymbolRefAction()); - ActionManager.instance.registerAction(new SymbolDefAction()); - ActionManager.instance.registerAction(new AskInputAction()); - ActionManager.instance.registerAction(new SymbolInFileAction()); - ActionManager.instance.registerAction(new CurrentDocumentAction()); - ActionManager.instance.registerAction(new SelectTextAction()); - ActionManager.instance.registerAction(new SelectBlockAction()); - ActionManager.instance.registerAction(new RunVSCodeCommandAction()); - } - - return ActionManager.instance; - } - - public registerAction(action: Action): void { - const existAction = this.actions.find(a => a.name === action.name); - if (existAction) { - return ; - } - this.actions.push(action); - } - - public getActionList(): Action[] { - return this.actions; - } - - public async applyAction(actionName: string, content: { "command": string, content: string, fileName: string }): Promise { - const action = this.actions.find(action => action.name.trim() === actionName.trim()); - if (!action) { - logger.channel()?.info(`Action not found: ${actionName}`); - return {exitCode: -1, stdout: '', stderr: `${actionName} not found in action list: ${this.actions.map(action => action.name)}`}; - } - - logger.channel()?.info(`Apply action: ${actionName}`); - - // action.args define what args should be passed to handler - // for example: - // action.args = [ - // {"name": "arg1", "description": "arg1 description", "type": "string", "from": "content.fileName"}, - // {"name": "arg2", "description": "arg2 description", "type": "string", "from": "content.content.v1"}] - // then: - // arg1 = content.fileName - // arg2 = content.content.v1 - // before use content.content.v1, we should parse content.content first as json - - if (action.args === undefined || action.args.length === 0) { - // every action should have args, if not, then it is invalid - logger.channel()?.error(`Action ${actionName} has no args`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `Action ${actionName} has no args`}; - } - - // construct args for handler - let args: {[key: string]: any} = {}; - - // check whether action.args has x.x.x like from value - let hasDotDotFrom = false; - for (const arg of action.args) { - if (arg.from !== undefined) { - // if arg.from has two or more dot, then it is x.x.x - if (arg.from.split('.').length >= 3) { - hasDotDotFrom = true; - break; - } - } - } - - // if hasDotDotFrom is true, then parse content as json - if (hasDotDotFrom) { - try { - content.content = JSON.parse(content.content); - } catch (error) { - logger.channel()?.info(`Parse content as json failed: ${error}`); - return {exitCode: -1, stdout: '', stderr: `Parse content as json failed: ${error}`}; - } - } - - - - for (const arg of action.args) { - let argValue = ''; - if (arg.from !== undefined) { - // visit arg.from, it is string - let argFromValue: any = content; - const argFrom = arg.from.split('.'); - // first item of argFrom is content, so skip it - for (const argFromItem of argFrom.slice(1)) { - argFromValue = argFromValue[argFromItem]; - } - // if argFromValue is undefined, then it is invalid - if (argFromValue === undefined) { - logger.channel()?.error(`Action ${actionName} arg ${arg.name} from ${arg.from} is undefined`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `Action ${actionName} arg ${arg.name} from ${arg.from} is undefined`}; - } - argValue = argFromValue; - } - args[arg.name] = argValue; - } - - return await action.handlerAction(args); - } - - public async applyCommandAction(command: string, args: {[key: string]: any}) : Promise { - const action = this.actions.find(action => action.name.trim() === command.trim()); - if (!action) { - logger.channel()?.info(`Action not found: ${command}`); - return {exitCode: -1, stdout: '', stderr: `${command} not found in action list: ${this.actions.map(action => action.name)}`}; - } - - logger.channel()?.info(`Apply command action: ${command}`); - - return await action.handlerAction(args); - } - - public loadCustomActions(workflowsDir: string): void { - const customActionsInstance = CustomActions.getInstance(); - customActionsInstance.parseActions(workflowsDir); - - for (const customAction of customActionsInstance.getActions()) { - const chatAction: Action = customAction; - this.registerAction(chatAction); - } - } - - public actionInstruction(): string { - let functionsDefList : {[key: string]: any}[] = []; - for (const action of this.actions) { - try { - if (action.name === "command_run") { - continue; - } - functionsDefList.push(getActionInstruction(action)); - } catch (error) { - logger.channel()?.error(`Failed to get action instruction: ${error}`); - logger.channel()?.show(); - } - } - - // return as json string - return JSON.stringify(functionsDefList, null, 4); - } - - public saveActionInstructionFile(tarFile: string): void { - try { - fs.writeFileSync(tarFile, this.actionInstruction()); - } catch (error) { - logger.channel()?.error(`Failed to save action instruction file: ${error}`); - logger.channel()?.show(); - } - } -} \ No newline at end of file diff --git a/src/action/askInputAction.ts b/src/action/askInputAction.ts deleted file mode 100644 index 064af80..0000000 --- a/src/action/askInputAction.ts +++ /dev/null @@ -1,48 +0,0 @@ - -import { Action, CustomActions } from './customAction'; - -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; -import { UiUtilVscode } from '../util/uiUtil_vscode'; -import { UiUtilWrapper } from '../util/uiUtil'; - - -export class AskInputAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "from": string }[]; - - constructor() { - this.name = 'ask_input'; - this.description = 'Ask user a question to when you need the user to input something'; - this.type = ['question']; - this.action = 'ask_input'; - this.handler = []; - this.args = [ - {"name": "question", "description": "The question you asked.", "type": "string", "from": "content.content.question"}, - ]; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const question = args.question; - - const answer: string | undefined = await UiUtilWrapper.showInputBox({ - title: question, - placeHolder: "Please input your answer here." - }); - if (answer === undefined) { - return {exitCode: -1, stdout: '', stderr: ``}; - } else { - return {exitCode: 0, stdout: answer, stderr: ""}; - } - } catch (error) { - logger.channel()?.error(`${this.name} handle error: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`}; - } - } -}; \ No newline at end of file diff --git a/src/action/currentDocumentAction.ts b/src/action/currentDocumentAction.ts deleted file mode 100644 index e29ce7a..0000000 --- a/src/action/currentDocumentAction.ts +++ /dev/null @@ -1,51 +0,0 @@ - -import * as vscode from 'vscode'; - -import { Action } from './customAction'; -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; -import * as fs from 'fs'; - -export class CurrentDocumentAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "required": boolean, "from": string }[]; - - constructor() { - this.name = 'current_document'; - this.description = 'Get current active document'; - this.type = ['None']; - this.action = 'current_document'; - this.handler = []; - this.args = []; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const editors = vscode.window.visibleTextEditors; - // get editor with file existed in file system - const editor = editors.find(editor => fs.existsSync(editor.document.fileName)); - - if (editor) { - const documentText = editor.document.getText(); - - const data = { - path: editor.document.fileName, - content: documentText - }; - const jsonData = JSON.stringify(data); - - return {exitCode: 0, stdout: JSON.stringify(data), stderr: ""}; - } else { - return {exitCode: -1, stdout: "", stderr: "No active editor"}; - } - } catch (error) { - logger.channel()?.error(`${this.name} handle error: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`}; - } - } -}; \ No newline at end of file diff --git a/src/action/customAction.ts b/src/action/customAction.ts deleted file mode 100644 index a275b63..0000000 --- a/src/action/customAction.ts +++ /dev/null @@ -1,155 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { logger } from '../util/logger'; -import { CommandResult, createTempSubdirectory, runCommandAndWriteOutput, runCommandStringAndWriteOutput } from '../util/commonUtil'; -import { UiUtilWrapper } from '../util/uiUtil'; - -export interface Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "from": string }[]; - - handlerAction: (args: { [key: string]: string }) => Promise; -} - -// generate instruction for action -export function getActionInstruction(action: Action): { name: string, description: string, parameters: any } { - logger.channel()?.info(`Action Name: ${action.name}`); - const actionSchema = { - name: action.name, - description: action.description, - parameters: { - type: "object", - properties: action.args.reduce((obj: any, arg: any) => { - obj[arg.name] = { - type: arg.type, - description: arg.description - }; - return obj; - }, {}), - required: action.args.filter((arg: any) => arg.required).map((arg: any) => arg.name) - } - }; - - return actionSchema; -} - -export class CustomActions { - private static instance: CustomActions | null = null; - private actions: Action[] = []; - - private constructor() { - } - - public static getInstance(): CustomActions { - if (!CustomActions.instance) { - CustomActions.instance = new CustomActions(); - } - return CustomActions.instance; - } - - public parseActions(workflowsDir: string): void { - this.actions = []; - - try { - const extensionDirs = fs.readdirSync(workflowsDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); - - for (const extensionDir of extensionDirs) { - const actionDir = path.join(workflowsDir, extensionDir, 'action'); - if (fs.existsSync(actionDir)) { - const actionSubDirs = fs.readdirSync(actionDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); - - for (const actionSubDir of actionSubDirs) { - const settingsPath = path.join(actionDir, actionSubDir, '_setting_.json'); - if (fs.existsSync(settingsPath)) { - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - const action: Action = { - name: settings.name, - description: settings.description, - type: settings.type, - action: settings.action, - args: settings.args, - handler: settings.handler.map((handler: string) => handler.replace('${CurDir}', path.join(actionDir, actionSubDir))), - - handlerAction: async (args: { [key: string]: string }) => { - // Implement the handler logic for the custom action - const tempDir = await createTempSubdirectory('devchat/action'); - const tempFile = path.join(tempDir, 'apply.json'); - - if (UiUtilWrapper.getConfiguration('DevChat', 'PythonForCommands')) { - args['PythonForCommands'] = UiUtilWrapper.getConfiguration('DevChat', 'PythonForCommands')!; - } else { - args['PythonForCommands'] = 'python'; - } - - const contextMap = { - 'codeBlock': args, - 'workspaceDir': UiUtilWrapper.workspaceFoldersFirstPath(), - 'activeFile': UiUtilWrapper.activeFilePath(), - 'selectRang': UiUtilWrapper.selectRange(), - 'secectText': UiUtilWrapper.selectText(), - }; - - // Save contextMap to temp file - await UiUtilWrapper.writeFile(tempFile, JSON.stringify(contextMap)); - - // replace ${contextFile} with tempFile for arg in handler - let handlerArgs = action.handler.map(arg => arg.replace('${contextFile}', tempFile)); - if (args !== undefined) { - // visit args, it is {[key: string]: string} - for (const arg in args) { - let argValue = args[arg]; - const argDefine = action.args.find(v => v.name === arg); - if (argDefine !== undefined && argDefine.as !== undefined) { - // save argValue to temp file - const tempFile = path.join(tempDir, argDefine.as); - await UiUtilWrapper.writeFile(tempFile, argValue); - argValue = tempFile; - } - // replace ${arg} with commandObj.args[arg] - handlerArgs = handlerArgs.map(v => { if (v === '${' + arg + '}') { return argValue; } else { return v; } }); - } - } - handlerArgs = handlerArgs.flat(); - - // run handler - let result: CommandResult = { exitCode: -1, stdout: '', stderr: '' }; - if (handlerArgs.length === 1) { - result = await runCommandStringAndWriteOutput(handlerArgs[0], undefined); - } else if (handlerArgs.length > 1) { - result = await runCommandAndWriteOutput(handlerArgs[0], handlerArgs.slice(1), undefined); - } - logger.channel()?.info(`Apply action: ${action.name} exit code:`, result.exitCode); - logger.channel()?.info(`stdout:`, result.stdout); - logger.channel()?.info(`stderr:`, result.stderr); - - // remove temp file - if (fs.existsSync(tempFile)) { - fs.unlinkSync(tempFile); - } - return result; - }, - }; - this.actions.push(action); - } - } - } - } - } catch (error) { - // Show error message - logger.channel()?.error(`Failed to parse actions: ${error}`); - logger.channel()?.show(); - } - } - - public getActions(): Action[] { - return this.actions; - } -} \ No newline at end of file diff --git a/src/action/runVSCodeCommand.ts b/src/action/runVSCodeCommand.ts deleted file mode 100644 index 649a935..0000000 --- a/src/action/runVSCodeCommand.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -执行vscode command,类名:RunVSCommandAction -*/ - -import * as vscode from 'vscode'; -import { Action } from './customAction'; -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; - -export class RunVSCodeCommandAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "required": boolean, "from": string }[]; - - constructor() { - this.name = 'run_vscode_command'; - this.description = 'Run VSCode command'; - this.type = ['command']; - this.action = 'run_vscode_command'; - this.handler = []; - this.args = [ - { - "name": "command", - "description": 'VSCode command to run.', - "type": "string", - "required": true, - "from": "content.content.command" - }, - { - "name": "args", - "description": 'Arguments for the command, separated by comma.', - "type": "string", - "required": false, - "from": "content.content.args" - } - ]; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const commandArgs = args.args ? args.args.split(',') : []; - if (args.command === 'vscode.open' && commandArgs.length > 0) { - commandArgs[0] = vscode.Uri.file(commandArgs[0]); - } - const result = await vscode.commands.executeCommand(args.command, ...commandArgs); - return {exitCode: 0, stdout: JSON.stringify(result), stderr: ""}; - } catch (error) { - logger.channel()?.error(`Failed to run VSCode command: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `Failed to run VSCode command: ${error}`}; - } - } -} \ No newline at end of file diff --git a/src/action/selectContextAction.ts b/src/action/selectContextAction.ts deleted file mode 100644 index 4518fbd..0000000 --- a/src/action/selectContextAction.ts +++ /dev/null @@ -1,133 +0,0 @@ - -import * as vscode from 'vscode'; - -import { Action } from './customAction'; -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; -import * as fs from 'fs'; - -export class SelectTextAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "required": boolean, "from": string }[]; - - constructor() { - this.name = 'select_text'; - this.description = 'Selected text in active document, only include text user selected.'; - this.type = ['None']; - this.action = 'select_text'; - this.handler = []; - this.args = []; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const editors = vscode.window.visibleTextEditors; - // get editor with file existed in file system - const editor = editors.find(editor => fs.existsSync(editor.document.fileName)); - - - if (editor) { - const selectedText = editor.document.getText(editor.selection); - - const data = { - path: editor.document.fileName, - startLine: editor.selection.start.line, - startColumn: editor.selection.start.character, - endLine: editor.selection.end.line, - endColumn: editor.selection.end.character, - content: selectedText - }; - - return {exitCode: 0, stdout: JSON.stringify(data), stderr: ""}; - } else { - return {exitCode: -1, stdout: "", stderr: "No active editor"}; - } - } catch (error) { - logger.channel()?.error(`${this.name} handle error: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`}; - } - } -}; - -export class SelectBlockAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "required": boolean, "from": string }[]; - - constructor() { - this.name = 'select_block'; - this.description = 'Select block in active document. For example, select a function name, then return the whole function.'; - this.type = ['None']; - this.action = 'select_block'; - this.handler = []; - this.args = []; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const editors = vscode.window.visibleTextEditors; - // get editor with file existed in file system - const editor:vscode.TextEditor | undefined = editors.find(editor => fs.existsSync(editor.document.fileName)); - if (!editor) { - return {exitCode: -1, stdout: "", stderr: "No active editor"}; - } - - // get all symbols in active document - const symbolsT: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( - 'vscode.executeDocumentSymbolProvider', - editor.document.uri - ); - if (!symbolsT) { - return {exitCode: -1, stdout: "", stderr: "No parent block found"}; - } - - let symbolsList: vscode.DocumentSymbol[] = []; - const visitSymbol = (symbol: vscode.DocumentSymbol) => { - symbolsList.push(symbol); - if (symbol.children) { - for (const child of symbol.children) { - visitSymbol(child); - } - } - }; - for (const symbol of symbolsT) { - visitSymbol(symbol); - } - - // visit symbolsList, and find the symbol which contains the selected text - let symbol: vscode.DocumentSymbol | undefined = undefined; - for (const symbolT of symbolsList.reverse()) { - if (symbolT.range.contains(editor.selection)) { - symbol = symbolT; - break; - } - } - - if (!symbol) { - return {exitCode: -1, stdout: "", stderr: "No parent block found"}; - } - - const data = { - path: editor.document.fileName, - startLine: symbol.range.start.line, - startColumn: symbol.range.start.character, - endLine: symbol.range.end.line, - endColumn: symbol.range.end.character, - content: editor.document.getText(symbol.range) - }; - return {exitCode: 0, stdout: JSON.stringify(data), stderr: ""}; - } catch (error) { - logger.channel()?.error(`${this.name} handle error: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`}; - } - } -}; \ No newline at end of file diff --git a/src/action/symbolDefAction.ts b/src/action/symbolDefAction.ts deleted file mode 100644 index 39ab732..0000000 --- a/src/action/symbolDefAction.ts +++ /dev/null @@ -1,179 +0,0 @@ - -import * as vscode from 'vscode'; - -import { Action, CustomActions } from './customAction'; - -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; -import { handleCodeSelected } from '../context/contextCodeSelected'; - -import * as fs from 'fs'; -import * as util from 'util'; -import { UiUtilWrapper } from '../util/uiUtil'; - -import path from 'path'; -import { getSymbolPosition } from './symbolRefAction'; - -const readFile = util.promisify(fs.readFile); - - -async function findSymbolInWorkspace(symbolName: string, symbolline: number, symbolFile: string): Promise { - const symbolPosition = await getSymbolPosition(symbolName, symbolline, symbolFile); - if (!symbolPosition) { - return []; - } - - // get definition of symbol - const refLocations = await vscode.commands.executeCommand( - 'vscode.executeDefinitionProvider', - vscode.Uri.file(symbolFile), - symbolPosition - ); - if (!refLocations) { - return []; - } - - // get related source lines - const contextListPromises = refLocations.map(async (refLocation) => { - let refLocationFile: string; - let targetRange: vscode.Range; - - if (refLocation instanceof vscode.Location) { - refLocationFile = refLocation.uri.fsPath; - targetRange = refLocation.range; - - // get symbols in file - const symbols = await vscode.commands.executeCommand( - 'vscode.executeDocumentSymbolProvider', - vscode.Uri.file(refLocationFile) - ); - - // find the smallest symbol definition that contains the target range - let smallestSymbol: vscode.DocumentSymbol | undefined; - for (const symbol of symbols) { - smallestSymbol = findSmallestSymbol(symbol, targetRange, smallestSymbol); - } - - if (smallestSymbol) { - targetRange = smallestSymbol.range; - } - } else { - refLocationFile = refLocation.targetUri.fsPath; - targetRange = refLocation.targetRange; - } - - const documentNew = await vscode.workspace.openTextDocument(refLocationFile); - - const data = { - path: refLocationFile, - start_line: targetRange.start.line, - end_line: targetRange.end.line, - content: documentNew.getText(targetRange) - }; - return JSON.stringify(data); - }); - - const contextList = await Promise.all(contextListPromises); - - return contextList; -} - -function findSmallestSymbol(symbol: vscode.DocumentSymbol, targetRange: vscode.Range, smallestSymbol: vscode.DocumentSymbol | undefined): vscode.DocumentSymbol | undefined { - if (symbol.range.contains(targetRange) && - (!smallestSymbol || smallestSymbol.range.contains(symbol.range))) { - smallestSymbol = symbol; - } - - for (const child of symbol.children) { - smallestSymbol = findSmallestSymbol(child, targetRange, smallestSymbol); - } - - return smallestSymbol; -} - -export class SymbolDefAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "required": boolean, "from": string }[]; - - constructor() { - this.name = 'symbol_def'; - this.description = ` - Function Purpose: This function retrieves the definition information for a given symbol. - Input: The symbol should not be in string format or in 'a.b' format. To find the definition of 'a.b', simply refer to 'b'. - Output: The function returns a dictionary with the following keys: - 'exitCode': If 'exitCode' is 0, the function execution was successful. If 'exitCode' is not 0, the function execution failed. - 'stdout': If the function executes successfully, 'stdout' is a list of JSON strings. Each JSON string contains: - 'path': The file path. - 'start_line': The start line of the content. - 'end_line': The end line of the content. - 'content': The source code related to the definition. - 'stderr': Error output if any. - Error Handling: If the function execution fails, 'exitCode' will not be 0 and 'stderr' will contain the error information.`; - this.type = ['symbol']; - this.action = 'symbol_def'; - this.handler = []; - this.args = [ - { - "name": "symbol", - "description": "The symbol variable specifies the symbol for which definition information is to be retrieved.", - "type": "string", - "required": true, - "from": "content.content.symbol" - }, { - "name": "line", - "description": 'The line variable specifies the line number of the symbol for which definition information is to be retrieved.', - "type": "number", - "required": true, - "from": "content.content.line" - }, { - "name": "file", - "description": 'File contain that symbol.', - "type": "string", - "required": true, - "from": "content.content.file" - } - ]; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const symbolName = args.symbol; - const symbolLine = args.line; - let symbolFile = args.file; - - // Check if the symbol name is valid - if (!symbolName || typeof symbolName !== 'string') { - throw new Error('Invalid symbol name. It should be a non-empty string.'); - } - - // Check if the symbol line is valid - if (!symbolLine || typeof symbolLine !== 'number' || symbolLine < 0) { - throw new Error('Invalid symbol line. It should be a non-negative number.'); - } - - // Check if the symbol file is valid - if (!symbolFile || typeof symbolFile !== 'string') { - throw new Error('Invalid symbol file. It should be a non-empty string.'); - } - - // if symbolFile is not absolute path, then get it's absolute path - if (!path.isAbsolute(symbolFile)) { - const basePath = UiUtilWrapper.workspaceFoldersFirstPath(); - symbolFile = path.join(basePath!, symbolFile); - } - - // get reference information - const refList = await findSymbolInWorkspace(symbolName, symbolLine, symbolFile); - - return {exitCode: 0, stdout: JSON.stringify(refList), stderr: ""}; - } catch (error) { - logger.channel()?.error(`${this.name} handle error: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`}; - } -} -}; \ No newline at end of file diff --git a/src/action/symbolInFileAction.ts b/src/action/symbolInFileAction.ts deleted file mode 100644 index 1e3704d..0000000 --- a/src/action/symbolInFileAction.ts +++ /dev/null @@ -1,96 +0,0 @@ - -import * as vscode from 'vscode'; - -import { Action } from './customAction'; - -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; -import * as path from 'path'; -import { UiUtilWrapper } from '../util/uiUtil'; - - -export class SymbolInFileAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "required": boolean, "from": string }[]; - - constructor() { - this.name = 'symbol_in_file'; - this.description = 'Retrieve definitions in file'; - this.type = ['symbol']; - this.action = 'symbol_ref'; - this.handler = []; - this.args = [ - { - "name": "file", - "description": 'Specify which file to load.', - "type": "string", - "required": true, - "from": "content.content.file" - } - ]; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - let symbolFile = args.file; - - // if symbolFile is not absolute path, then get it's absolute path - if (!path.isAbsolute(symbolFile)) { - const basePath = UiUtilWrapper.workspaceFoldersFirstPath(); - symbolFile = path.join(basePath!, symbolFile); - } - - // load symbols in file - const symbolsT: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( - 'vscode.executeDocumentSymbolProvider', - vscode.Uri.file(symbolFile) - ); - let symbolsList: vscode.DocumentSymbol[] = []; - const visitSymbol = (symbol: vscode.DocumentSymbol) => { - symbolsList.push(symbol); - for (const child of symbol.children) { - visitSymbol(child); - } - } - for (const symbol of symbolsT) { - visitSymbol(symbol); - } - - // convert symbolsList to json object list - - let sysbolsOjbs: {[key: string]: any}[] = []; - symbolsList.forEach(symbol => { - sysbolsOjbs.push({ - "name": symbol.name, - "kind": vscode.SymbolKind[symbol.kind], - "startPosition": { - "line": symbol.range.start.line, - "column": symbol.range.start.character - }, - "endPosition": { - "line": symbol.range.end.line, - "column": symbol.range.end.character - }, - "selectRangeStartPosition": { - "line": symbol.selectionRange.start.line, - "column": symbol.selectionRange.start.character - }, - "selectRangeEndPosition": { - "line": symbol.selectionRange.end.line, - "column": symbol.selectionRange.end.character - } - }); - }); - - return {exitCode: 0, stdout: JSON.stringify(sysbolsOjbs), stderr: ""}; - } catch (error) { - logger.channel()?.error(`${this.name} handle error: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`}; - } - } -}; \ No newline at end of file diff --git a/src/action/symbolRefAction.ts b/src/action/symbolRefAction.ts deleted file mode 100644 index c3b9816..0000000 --- a/src/action/symbolRefAction.ts +++ /dev/null @@ -1,257 +0,0 @@ - -import * as vscode from 'vscode'; - -import { Action, CustomActions } from './customAction'; - -import { CommandResult } from '../util/commonUtil'; -import { logger } from '../util/logger'; -import { handleCodeSelected } from '../context/contextCodeSelected'; - -import * as fs from 'fs'; -import * as util from 'util'; -import { UiUtilWrapper } from '../util/uiUtil'; -import path from 'path'; - -const readFile = util.promisify(fs.readFile); - - -async function isCorrectIndexSymbol(filename: string, position: vscode.Position, symbolName: string): Promise< boolean > { - let defLocations = await vscode.commands.executeCommand( - 'vscode.executeDefinitionProvider', - vscode.Uri.file(filename), - position - ); - - if (!defLocations || defLocations.length === 0) { - defLocations = await vscode.commands.executeCommand( - 'vscode.executeReferenceProvider', - vscode.Uri.file(filename), - position - ); - } - - if (!defLocations) { - return false; - } - - for (const defLocation of defLocations) { - let range = undefined; - let uri = undefined; - if (defLocation.targetSelectionRange) { - range = defLocation.targetSelectionRange; - uri = defLocation.targetUri; - } else if (defLocation.targetRange) { - range = defLocation.targetRange; - uri = defLocation.targetUri; - } else { - range = defLocation.range; - uri = defLocation.uri; - } - if (!range) { - continue; - } - - const documentNew = await vscode.workspace.openTextDocument(uri); - const sbName = await documentNew.getText(range); - if (sbName === symbolName) { - return true; - } - } - return false; -} - -export async function getSymbolPosition(symbolName: string, symbolLine: number, symbolFile: string): Promise { - // Read the file - let content = await readFile(symbolFile, 'utf-8'); - - // Split the content into lines - let lines = content.split('\n'); - - // Check if the line number is valid - if (symbolLine < 0 || symbolLine >= lines.length) { - return undefined; - } - - // Get the line text - let symbolIndex = -1; - const maxLine = lines.length < symbolLine + 6 ? lines.length : symbolLine + 6; - for (let i = symbolLine; i < maxLine; i++) { - let lineText = lines[i]; - - // Find the symbol in the line - let lineOffsetPos = -1; - while (true) { - symbolIndex = lineText.indexOf(symbolName, lineOffsetPos+1); - if (symbolIndex > -1 && await isCorrectIndexSymbol(symbolFile, new vscode.Position(i, symbolIndex), symbolName)) { - return new vscode.Position(i, symbolIndex); - } - if (symbolIndex === -1) { - break; - } - lineOffsetPos = symbolIndex; - } - } - - return undefined; -} - -async function findSymbolInWorkspace(symbolName: string, symbolline: number, symbolFile: string): Promise { - const symbolPosition = await getSymbolPosition(symbolName, symbolline, symbolFile); - if (!symbolPosition) { - throw new Error(`Symbol "${symbolName}" not found in file "${symbolFile}" at line ${symbolline}.`); - } - - logger.channel()?.info(`symbol position: ${symbolPosition.line}:${symbolPosition.character}`); - - // get all references of symbol - const refLocations = await vscode.commands.executeCommand( - 'vscode.executeReferenceProvider', - vscode.Uri.file(symbolFile), - symbolPosition - ); - if (!refLocations || refLocations.length === 0) { - throw new Error(`No references found for symbol "${symbolName}" in file "${symbolFile}" at line ${symbolline}.`); - } - - // get related source lines - let contextList: Set = new Set(); - for (const refLocation of refLocations) { - const refLocationFile = refLocation.uri.fsPath; - - // calculate the line number, if refLocation.range.start.line - 2 < 0, then set it to 0 - const startLine = refLocation.range.start.line - 2 < 0 ? 0 : refLocation.range.start.line - 2; - - const documentNew = await vscode.workspace.openTextDocument(refLocationFile); - const rangeNew = new vscode.Range(startLine, 0, refLocation.range.end.line + 2, 10000); - - // get symbol define in symbolFile - const symbolsT: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( - 'vscode.executeDocumentSymbolProvider', - refLocation.uri - ); - let symbolsList: vscode.DocumentSymbol[] = []; - const visitSymbol = (symbol: vscode.DocumentSymbol) => { - symbolsList.push(symbol); - if (symbol.children) { - for (const child of symbol.children) { - visitSymbol(child); - } - } - }; - if (symbolsT) { - for (const symbol of symbolsT) { - visitSymbol(symbol); - } - } - - let symbol: vscode.DocumentSymbol | undefined = undefined; - for (const symbolT of symbolsList.reverse()) { - if (symbolT.range.contains(refLocation.range)) { - symbol = symbolT; - break; - } - } - - const symbolName = symbol ? symbol.name : ''; - const symbolLine = symbol ? symbol.range.start.line : 0; - - const data = { - path: refLocationFile, - start_line: startLine, - ref_line: refLocation.range.start.line, - content: documentNew.getText(rangeNew), - parentDefine: symbolName, - parentDefineStartLine: symbolLine - }; - contextList.add(JSON.stringify(data)); - } - - return Array.from(contextList); -} -export class SymbolRefAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { "name": string, "description": string, "type": string, "as"?: string, "required": boolean, "from": string }[]; - - constructor() { - this.name = 'symbol_ref'; - this.description = ` - Function Purpose: This function retrieves the reference information for a given symbol. - Input: The symbol should not be in string format or in 'a.b' format. To find the reference of 'a.b', simply refer to 'b'. - Output: The function returns a dictionary with the following keys: - 'exitCode': If 'exitCode' is 0, the function execution was successful. If 'exitCode' is not 0, the function execution failed. - 'stdout': If the function executes successfully, 'stdout' is a list of JSON strings. Each JSON string contains: - 'path': The file path. - 'ref_line': The ref line of the symbol. - 'content': The source code related to the reference. - 'parentDefine': The parent symbol name of the reference, which is always a function name or class name. - 'parentDefineStartLine': The start line of the parent symbol name of the reference. - 'stderr': Error output if any. - Error Handling: If the function execution fails, 'exitCode' will not be 0 and 'stderr' will contain the error information.`; - this.type = ['symbol']; - this.action = 'symbol_ref'; - this.handler = []; - this.args = [ - { - "name": "symbol", - "description": "The symbol variable specifies the symbol for which reference information is to be retrieved.", - "type": "string", - "required": true, - "from": "content.content.symbol" - }, { - "name": "line", - "description": 'The line variable specifies the line number of the symbol for which reference information is to be retrieved.', - "type": "number", - "required": true, - "from": "content.content.line" - }, { - "name": "file", - "description": 'File contain that symbol.', - "type": "string", - "required": true, - "from": "content.content.file" - } - ]; - } - - async handlerAction(args: {[key: string]: any}): Promise { - try { - const symbolName = args.symbol; - const symbolLine = args.line; - let symbolFile = args.file; - - // Check if the symbol name is valid - if (!symbolName || typeof symbolName !== 'string') { - throw new Error('Invalid symbol name. It should be a non-empty string.'); - } - - // Check if the symbol line is valid - if (!symbolLine || typeof symbolLine !== 'number' || symbolLine < 0) { - throw new Error('Invalid symbol line. It should be a non-negative number.'); - } - - // Check if the symbol file is valid - if (!symbolFile || typeof symbolFile !== 'string') { - throw new Error('Invalid symbol file. It should be a non-empty string.'); - } - - // if symbolFile is not absolute path, then get it's absolute path - if (!path.isAbsolute(symbolFile)) { - const basePath = UiUtilWrapper.workspaceFoldersFirstPath(); - symbolFile = path.join(basePath!, symbolFile); - } - - // get reference information - const refList = await findSymbolInWorkspace(symbolName, symbolLine, symbolFile); - - return {exitCode: 0, stdout: JSON.stringify(refList), stderr: ""}; - } catch (error) { - logger.channel()?.error(`${this.name} handle error: ${error}`); - logger.channel()?.show(); - return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`}; - } -} -}; \ No newline at end of file diff --git a/src/command/commandManager.ts b/src/command/commandManager.ts deleted file mode 100644 index c90757f..0000000 --- a/src/command/commandManager.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { createTempSubdirectory } from "../util/commonUtil"; -import DevChat from "../toolwrapper/devchat"; -import { FT } from "../util/feature_flags/feature_toggles"; -import CustomCommands from "./customCommand"; -import * as path from "path"; -import * as fs from 'fs'; - -export interface Command { - name: string; - pattern: string; - description: string; - args: number; - handler: (commandName: string, userInput: string) => Promise; -} - -class CommandManager { - private static instance: CommandManager; - private commands: Command[] = []; - - private constructor() { } - - public static getInstance(): CommandManager { - if (!CommandManager.instance) { - CommandManager.instance = new CommandManager(); - } - - return CommandManager.instance; - } - - registerCommand(command: Command): void { - this.commands.push(command); - } - - getCommandList(includeHide: boolean = false): Command[] { - // load commands from CustomCommands - let newCommands: Command[] = [...this.commands]; - const customCommands = CustomCommands.getInstance(); - const commands = customCommands.getCommands(); - commands.forEach(command => { - const commandObj: Command = { - name: command.name, - pattern: command.pattern, - description: command.description, - args: command.args, - handler: async (commandName: string, userInput: string) => { - return CustomCommands.getInstance().handleCommand(commandName, userInput); - } - }; - if (command.show || includeHide) { - newCommands.push(commandObj); - } - }); - return newCommands; - } - - async getCommandListByDevChatRun(includeHide: boolean = false): Promise { - // load commands from CustomCommands - let newCommands: Command[] = [...this.commands]; - - const devChat = new DevChat(); - const commandList = await devChat.commands(); - commandList.forEach(command => { - const commandObj: Command = { - name: command.name, - pattern: command.name, - description: command.description, - args: 0, - handler: async (commandName: string, userInput: string) => { - const tempDir = await createTempSubdirectory('devchat/command'); - const tempFile = path.join(tempDir, command.name); - const stdout = await devChat.commandPrompt(command.name); - fs.writeFileSync(tempFile, stdout); - return `[instruction|${tempFile}] `; - } - }; - newCommands.push(commandObj); - }); - - return newCommands; - } - - async processTextBak(text: string): Promise { - // 定义一个异步函数来处理单个命令 - const processCommand = async (commandObj: Command, userInput: string) => { - // 转义特殊字符 - let commandPattern: RegExp; - if (commandObj.pattern.indexOf("{{") > 0) { - const escapedPattern = commandObj.pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - commandPattern = new RegExp( - `\\/(${escapedPattern.replace('\\{\\{prompt\\}\\}', '\\{\\{(.+?)\\}\\}')})`, - 'g' - ); - } else { - const escapedPattern = commandObj.pattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); - // Update the regex pattern to match commands ending with space or newline - commandPattern = new RegExp( - `\\/(?${escapedPattern.replace('{{prompt}}', '(?.+?)')})(?=\\s|\\n|$)`, - 'g' - ); - } - - - const matches = Array.from(text.matchAll(commandPattern)); - const replacements = await Promise.all( - matches.map(async (match) => { - const matchedUserInput = commandObj.pattern.indexOf("{{") > 0 ? match[2] : match.groups!.userInput; - return await commandObj.handler(commandObj.name, matchedUserInput); - }) - ); - - let result = userInput; - for (let i = 0; i < matches.length; i++) { - result = result.replace(matches[i][0], replacements[i]); - } - return result; - }; - - // 处理所有命令 - let result = text; - for (const commandObj of await this.getCommandListByDevChatRun()) { - result = await processCommand(commandObj, result); - } - - return result; - } - - async processText(text: string): Promise { - let result = text.trim(); - const messageTextArr = result.split(/ |\n|\t/); - const commandName = messageTextArr[0]; - - for (const commandObj of await this.getCommandListByDevChatRun()) { - const commandObjNamePattern = "/" + commandObj.name + " "; - if (commandObjNamePattern === commandName) { - const newInstructFile = await commandObj.handler(commandObj.name, ""); - result = newInstructFile + result; - break; - } - } - - return result; - } -} - -export default CommandManager; diff --git a/src/command/customCommand.ts b/src/command/customCommand.ts deleted file mode 100644 index 1869b2b..0000000 --- a/src/command/customCommand.ts +++ /dev/null @@ -1,130 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { logger } from '../util/logger'; - -export interface Command { - name: string; - pattern: string; - description: string; - message: string; - default: boolean; - show: boolean; - args: number; - instructions: string[]; -} - -class CustomCommands { - private static instance: CustomCommands | null = null; - private commands: Command[] = []; - - private constructor() { - } - - public static getInstance(): CustomCommands { - if (!CustomCommands.instance) { - CustomCommands.instance = new CustomCommands(); - } - return CustomCommands.instance; - } - - public parseCommands(workflowsDir: string): void { - this.commands = []; - - try { - const extensionDirs = fs.readdirSync(workflowsDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); - - for (const extensionDir of extensionDirs) { - const commandDir = path.join(workflowsDir, extensionDir, 'command'); - if (fs.existsSync(commandDir)) { - const commandSubDirs = fs.readdirSync(commandDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); - - for (const commandSubDir of commandSubDirs) { - const settingsPath = path.join(commandDir, commandSubDir, '_setting_.json'); - if (fs.existsSync(settingsPath)) { - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - const command: Command = { - name: commandSubDir, - pattern: settings.pattern, - description: settings.description, - message: settings.message, - default: settings.default, - args: settings.args === undefined ? 0 : settings.args, - show: settings.show === undefined ? "true" : settings.show, - instructions: settings.instructions.map((instruction: string) => path.join(commandDir, commandSubDir, instruction)) - }; - this.commands.push(command); - } - } - } - } - } catch (error) { - // 显示错误消息 - logger.channel()?.error(`Failed to parse commands due to error: ${error}`); - logger.channel()?.show(); - } - } - - public regCommand(command: Command) { - this.commands.push(command); - } - - public getCommands(): Command[] { - return this.commands; - } - - public getCommand(commandName: string): Command | null { - const foundCommand = this.commands.find(command => command.name === commandName); - return foundCommand ? foundCommand : null; - } - - - public handleCommand(commandName: string, userInput: string): string { - // 获取命令对象,这里假设您已经有一个方法或属性可以获取到命令对象 - const command = this.getCommand(commandName); - if (!command) { - logger.channel()?.error(`Command ${commandName} not found!`); - logger.channel()?.show(); - return ''; - } - - let commandMessage = command.message; - if (userInput && userInput.length > 0) { - // userInput is "['aa', 'bb]" like string - // parse userInput to array - // handle eval exception - - try { - const userInputArray = eval(userInput); - - // replace command message $1 with userInputArray[0], $2 with userInputArray[1] and so on - for (let i = 0; i < userInputArray.length; i++) { - commandMessage = commandMessage.replace(`$${i + 1}`, userInputArray[i]); - } - } catch (error) { - logger.channel()?.error(`Failed to parse user input '${userInput}' due to error: ${error}. A valid input should be in the format: ['aa', 'bb']`); - logger.channel()?.show(); - return ''; - } - } - - // replace ${Name} with enviroment var Name - const envVarRegex = /\${(\w+)}/g; - commandMessage = commandMessage.replace(envVarRegex, (match, p1) => { - return process.env[p1] || ''; - }); - - // build instrctions - const instructions = command!.instructions - .map((instruction: string) => `[instruction|${instruction}]`) - .join(' '); - - // 返回结果字符串 - return `${instructions} ${commandMessage}`; - } -} - -export default CustomCommands; diff --git a/src/command/loadCommands.ts b/src/command/loadCommands.ts deleted file mode 100644 index e4dab74..0000000 --- a/src/command/loadCommands.ts +++ /dev/null @@ -1,6 +0,0 @@ -import CommandManager from './commandManager'; - -const commandManager = CommandManager.getInstance(); - -// 注册命令 - diff --git a/src/context/contextManager.ts b/src/context/contextManager.ts index f85e6e3..df6f847 100644 --- a/src/context/contextManager.ts +++ b/src/context/contextManager.ts @@ -12,7 +12,7 @@ export interface ChatContext { handler: () => Promise; } - class ChatContextManager { +export class ChatContextManager { private static instance: ChatContextManager; private contexts: ChatContext[] = []; @@ -66,7 +66,7 @@ export interface ChatContext { return this.contexts; } - async processText(command: string): Promise { + async handleContextSelected(command: string): Promise { for (const contextObj of this.contexts) { if (contextObj.name === command) { return await contextObj.handler(); @@ -77,4 +77,3 @@ export interface ChatContext { } } - export default ChatContextManager; diff --git a/src/context/loadContexts.ts b/src/context/loadContexts.ts index 31d7ab3..0c00c94 100644 --- a/src/context/loadContexts.ts +++ b/src/context/loadContexts.ts @@ -1,4 +1,4 @@ -import ChatContextManager from './contextManager'; +import { ChatContextManager } from './contextManager'; import { gitDiffCachedContext } from './contextGitDiffCached'; import { gitDiffContext } from './contextGitDiff'; import { customCommandContext } from './contextCustomCommand'; diff --git a/src/contributes/commands.ts b/src/contributes/commands.ts index e3f2db5..bc897e7 100644 --- a/src/contributes/commands.ts +++ b/src/contributes/commands.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as os from 'os'; import { sendFileSelectMessage, sendCodeSelectMessage } from './util'; -import ExtensionContextHolder from '../util/extensionContext'; +import { ExtensionContextHolder } from '../util/extensionContext'; import { TopicManager } from '../topic/topicManager'; import { TopicTreeDataProvider, TopicTreeItem } from '../panel/topicView'; import { FilePairManager } from '../util/diffFilePairs'; @@ -11,15 +11,15 @@ import { UiUtilWrapper } from '../util/uiUtil'; import { isValidApiKey } from '../handler/historyMessagesBase'; import { logger } from '../util/logger'; -import { CommandRun} from '../util/commonUtil'; import path from 'path'; -import { sendCommandListByDevChatRun, updateChatModels } from '../handler/regCommandList'; +import { sendCommandListByDevChatRun, updateChatModels } from '../handler/workflowCommandHandler'; import DevChat from "../toolwrapper/devchat"; import { createEnvByConda, createEnvByMamba } from '../util/python_installer/app_install'; import { installRequirements } from '../util/python_installer/package_install'; + function registerOpenChatPanelCommand(context: vscode.ExtensionContext) { let disposable = vscode.commands.registerCommand('devchat.openChatPanel', async () => { await vscode.commands.executeCommand('devchat-view.focus'); @@ -40,7 +40,7 @@ function registerAddContextCommand(context: vscode.ExtensionContext) { await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.fsPath); }; - context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext', callback)); + context.subscriptions.push(vscode.commands.registerCommand('devchat.addContext', callback)); context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext_chinese', callback)); } @@ -266,7 +266,6 @@ export function registerInstallCommandsPython(context: vscode.ExtensionContext) // 2. check requirements.txt in ~/.chat dir // 3. install requirements.txt - // 1. install python >= 3.11 logger.channel()?.info(`create env for python ...`); logger.channel()?.info(`try to create env by mamba ...`); diff --git a/src/contributes/util.ts b/src/contributes/util.ts index 230546c..fdc2354 100644 --- a/src/contributes/util.ts +++ b/src/contributes/util.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import { handleCodeSelected } from '../context/contextCodeSelected'; import { handleFileSelected } from '../context/contextFileSelected'; import { MessageHandler } from '../handler/messageHandler'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; +import { regOutMessage } from '../util/reg_messages'; import { logger } from '../util/logger'; regOutMessage({command: 'appendContext', context: ''}); diff --git a/src/contributes/views.ts b/src/contributes/views.ts index 911b77f..1b92b3f 100644 --- a/src/contributes/views.ts +++ b/src/contributes/views.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { DevChatViewProvider } from '../panel/devchatView'; import { TopicTreeDataProvider } from '../panel/topicView'; -import ExtensionContextHolder from '../util/extensionContext'; +import { ExtensionContextHolder } from '../util/extensionContext'; export function regDevChatView(context: vscode.ExtensionContext) { diff --git a/src/extension.ts b/src/extension.ts index 234ace8..b003476 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,14 +21,14 @@ import { import { regLanguageContext } from './contributes/context'; import { regDevChatView, regTopicView } from './contributes/views'; -import ExtensionContextHolder from './util/extensionContext'; +import { ExtensionContextHolder } from './util/extensionContext'; import { logger } from './util/logger'; import { LoggerChannelVscode } from './util/logger_vscode'; import { createStatusBarItem } from './panel/statusBarView'; import { UiUtilWrapper } from './util/uiUtil'; import { UiUtilVscode } from './util/uiUtil_vscode'; -import { FT } from './util/feature_flags/feature_toggles'; import { ApiKeyManager } from './util/apiKey'; +import { startRpcServer } from './ide_services/services'; async function isProviderHasSetted() { try { @@ -241,5 +241,7 @@ async function activate(context: vscode.ExtensionContext) { regApplyDiffResultCommand(context); regPythonPathCommand(context); + + startRpcServer(); } exports.activate = activate; \ No newline at end of file diff --git a/src/handler/userAccessKey.ts b/src/handler/accessKeyHandler.ts similarity index 60% rename from src/handler/userAccessKey.ts rename to src/handler/accessKeyHandler.ts index 7f7857f..482026d 100644 --- a/src/handler/userAccessKey.ts +++ b/src/handler/accessKeyHandler.ts @@ -14,18 +14,22 @@ export async function getUserAccessKey(message: any, panel: vscode.WebviewPanel| const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const llmModelData = await ApiKeyManager.llmModel(); if (!llmModelData || !llmModelData.api_key) { - MessageHandler.sendMessage(panel, {"command": "getUserAccessKey", "accessKey": "", "keyType": "", "endPoint": ""}); + MessageHandler.sendMessage(panel, + { + "command": "getUserAccessKey", + "accessKey": "", + "keyType": "", + "endPoint": "" + } + ); return; } - let keyType: string = "others"; - if (llmModelData.api_key?.startsWith("DC.")) { - keyType = "DevChat"; - } - - let openAiApiBase = llmModelData.api_base; - if (!openAiApiBase) { - openAiApiBase = ""; - } - MessageHandler.sendMessage(panel, {"command": "getUserAccessKey", "accessKey": llmModelData.api_key, "keyType": keyType, "endPoint": openAiApiBase}); + const keyData = { + "command": "getUserAccessKey", + "accessKey": llmModelData.api_key, + "keyType": llmModelData.api_key?.startsWith("DC.") ? "DevChat" : "others", + "endPoint": llmModelData.api_base ? llmModelData.api_base : "" + }; + MessageHandler.sendMessage(panel, keyData); } \ No newline at end of file diff --git a/src/handler/addContext.ts b/src/handler/addContext.ts deleted file mode 100644 index 69bcfec..0000000 --- a/src/handler/addContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as vscode from 'vscode'; -import ChatContextManager from '../context/contextManager'; - -import { MessageHandler } from './messageHandler'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; - - -regInMessage({command: 'addContext', selected: ''}); -regOutMessage({command: 'appendContext', context: ''}); -export async function addConext(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - const contextStrList = await ChatContextManager.getInstance().processText(message.selected); - for (const contextStr of contextStrList) { - MessageHandler.sendMessage(panel, { command: 'appendContext', context: contextStr }); - } -} - - diff --git a/src/handler/addRefCommandContext.ts b/src/handler/addRefCommandContext.ts deleted file mode 100644 index ad4c3e8..0000000 --- a/src/handler/addRefCommandContext.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as vscode from 'vscode'; -import { handleRefCommand } from '../context/contextRef'; -import { MessageHandler } from './messageHandler'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; - - -regInMessage({command: 'addRefCommandContext', refCommand: ''}); -regOutMessage({command: 'appendContext', context: ''}); -// message: { command: 'addRefCommandContext', refCommand: string } -// User input: /ref ls . then "ls ." will be passed to refCommand -export async function addRefCommandContext(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - const contextStr = await handleRefCommand(message.refCommand); - MessageHandler.sendMessage(panel, { command: 'appendContext', context: contextStr }); - return; -} diff --git a/src/handler/applyAction.ts b/src/handler/applyAction.ts deleted file mode 100644 index 5ff2121..0000000 --- a/src/handler/applyAction.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as vscode from 'vscode'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; -import ActionManager from '../action/actionManager'; -import { MessageHandler } from './messageHandler'; -import { sendMessage } from './sendMessage'; -import { logger } from '../util/logger'; - -function compressText(text: string, maxLength: number): string { - if (text.length <= maxLength) { - return text; - } - - const halfLength = Math.floor(maxLength / 2); - return text.slice(0, halfLength) + " ... " + text.slice(-halfLength); -} - - -regInMessage({command: 'applyAction', actionName: '', parentHash: '', codeBlock: { type: '', content: '', fileName: ''}}); -export async function applyAction(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - try { - const result = await ActionManager.getInstance().applyAction("command_run", { "command": "", "fileName": message.fileName, "content": message.content }); - - // send error message to devchat - const commandObj = JSON.parse(message.content) - const newMessage = `{"exit_code": ${result.exitCode}, stdout: ${result.stdout}, stderr: ${result.stderr}}`; - MessageHandler.sendMessage(panel, { "command": "systemMessage", "text": "waitting command reponse..." }); - sendMessage({command: 'sendMessage', text: newMessage, parent_hash: message.parentHash}, panel, commandObj.name); - - } catch (error) { - logger.channel()?.error('Failed to parse code file content: ' + error); - logger.channel()?.show(); - } -} - - diff --git a/src/handler/codeApply.ts b/src/handler/codeApply.ts deleted file mode 100644 index 35f8b86..0000000 --- a/src/handler/codeApply.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as vscode from 'vscode'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; - - -export async function applyCode(text: string) { - const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); - - if (validVisibleTextEditors.length > 1) { - vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`); - return; - } - - const editor = validVisibleTextEditors[0]; - if (!editor) { - return; - } - - const selection = editor.selection; - const start = selection.start; - const end = selection.end; - - await editor.edit((editBuilder: vscode.TextEditorEdit) => { - editBuilder.replace(new vscode.Range(start, end), text); - }); -} - - -regInMessage({command: 'code_apply', content: ''}); -export async function codeApply(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - await applyCode(message.content); - return; -} - - diff --git a/src/handler/codeBlockHandler.ts b/src/handler/codeBlockHandler.ts new file mode 100644 index 0000000..dc8327d --- /dev/null +++ b/src/handler/codeBlockHandler.ts @@ -0,0 +1,94 @@ +import * as vscode from 'vscode'; +import { regInMessage, regOutMessage } from '../util/reg_messages'; + + +export async function applyCode(text: string) { + const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); + + if (validVisibleTextEditors.length > 1) { + vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`); + return; + } + + const editor = validVisibleTextEditors[0]; + if (!editor) { + return; + } + + const selection = editor.selection; + const start = selection.start; + const end = selection.end; + + await editor.edit((editBuilder: vscode.TextEditorEdit) => { + editBuilder.replace(new vscode.Range(start, end), text); + }); +} + + + +export async function applyCodeFile(text: string, fileName: string): Promise { + if (fileName) { + await replaceFileContent(vscode.Uri.file(fileName), text); + return; + } + + const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); + + if (validVisibleTextEditors.length > 1) { + vscode.window.showErrorMessage(`2There are more then one visible text editors. Please close all but one and try again.`); + return; + } + + const editor = validVisibleTextEditors[0]; + if (!editor) { + return; + } + + const document = editor.document; + const fullRange = new vscode.Range( + document.positionAt(0), + document.positionAt(document.getText().length) + ); + + await editor.edit((editBuilder: vscode.TextEditorEdit) => { + editBuilder.replace(fullRange, text); + }); +} +export async function replaceFileContent(uri: vscode.Uri, newContent: string) { + try { + // 创建一个 WorkspaceEdit 对象 + const workspaceEdit = new vscode.WorkspaceEdit(); + + // 获取文件的当前内容 + const document = await vscode.workspace.openTextDocument(uri); + + // 计算文件的完整范围(从文件开始到文件结束) + const fullRange = new vscode.Range( + document.positionAt(0), + document.positionAt(document.getText().length) + ); + + // 使用 WorkspaceEdit 的 replace 方法替换文件的完整范围内容 + workspaceEdit.replace(uri, fullRange, newContent); + + // 应用编辑更改 + await vscode.workspace.applyEdit(workspaceEdit); + + // 显示成功消息 + vscode.window.showInformationMessage('File content replaced successfully.'); + } catch (error) { + // 显示错误消息 + vscode.window.showErrorMessage('Failed to replace file content: ' + error); + } +} + + +regInMessage({command: 'code_apply', content: ''}); +export async function insertCodeBlockToFile(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { + await applyCode(message.content); +} + +regInMessage({command: 'code_file_apply', content: '', fileName: ''}); +export async function replaceCodeBlockToFile(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { + await applyCodeFile(message.content, message.fileName); +} diff --git a/src/handler/codeFileApply.ts b/src/handler/codeFileApply.ts deleted file mode 100644 index 865f6b0..0000000 --- a/src/handler/codeFileApply.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as vscode from 'vscode'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; -import ActionManager from '../action/actionManager'; -import { MessageHandler } from './messageHandler'; -import { sendMessage } from './sendMessage'; -import { logger } from '../util/logger'; - -function compressText(text: string, maxLength: number): string { - if (text.length <= maxLength) { - return text; - } - - const halfLength = Math.floor(maxLength / 2); - return text.slice(0, halfLength) + " ... " + text.slice(-halfLength); -} - -async function replaceFileContent(uri: vscode.Uri, newContent: string) { - try { - // 创建一个 WorkspaceEdit 对象 - const workspaceEdit = new vscode.WorkspaceEdit(); - - // 获取文件的当前内容 - const document = await vscode.workspace.openTextDocument(uri); - - // 计算文件的完整范围(从文件开始到文件结束) - const fullRange = new vscode.Range( - document.positionAt(0), - document.positionAt(document.getText().length) - ); - - // 使用 WorkspaceEdit 的 replace 方法替换文件的完整范围内容 - workspaceEdit.replace(uri, fullRange, newContent); - - // 应用编辑更改 - await vscode.workspace.applyEdit(workspaceEdit); - - // 显示成功消息 - vscode.window.showInformationMessage('File content replaced successfully.'); - } catch (error) { - // 显示错误消息 - vscode.window.showErrorMessage('Failed to replace file content: ' + error); - } -} - -export async function applyCodeFile(text: string, fileName: string): Promise { - if (fileName) { - await replaceFileContent(vscode.Uri.file(fileName), text); - return; - } - - const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); - - if (validVisibleTextEditors.length > 1) { - vscode.window.showErrorMessage(`2There are more then one visible text editors. Please close all but one and try again.`); - return; - } - - const editor = validVisibleTextEditors[0]; - if (!editor) { - return; - } - - const document = editor.document; - const fullRange = new vscode.Range( - document.positionAt(0), - document.positionAt(document.getText().length) - ); - - await editor.edit((editBuilder: vscode.TextEditorEdit) => { - editBuilder.replace(fullRange, text); - }); -} - -regInMessage({command: 'code_file_apply', content: '', fileName: ''}); -export async function codeFileApply(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - await applyCodeFile(message.content, message.fileName); - return; -} - - diff --git a/src/handler/doCommit.ts b/src/handler/commitHandler.ts similarity index 100% rename from src/handler/doCommit.ts rename to src/handler/commitHandler.ts diff --git a/src/handler/contextDetail.ts b/src/handler/contextDetail.ts index 44ad248..64c3915 100644 --- a/src/handler/contextDetail.ts +++ b/src/handler/contextDetail.ts @@ -1,20 +1,4 @@ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import { MessageHandler } from './messageHandler'; import { regInMessage, regOutMessage } from '../util/reg_messages'; -import { logger } from '../util/logger'; -regInMessage({ command: 'contextDetail', file: '' }); -regOutMessage({ command: 'contextDetailResponse', file: '', result: '' }); -// message: { command: 'contextDetail', file: string } -// read detail context information from file -// return json string -export async function contextDetail(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { - try { - const fileContent = fs.readFileSync(message.file, 'utf-8'); - MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file': message.file, result: fileContent }); - } catch (error) { - logger.channel()?.error(`Error reading file ${message.file}: ${error}`); - } -} + diff --git a/src/handler/contextHandler.ts b/src/handler/contextHandler.ts new file mode 100644 index 0000000..1d728bd --- /dev/null +++ b/src/handler/contextHandler.ts @@ -0,0 +1,33 @@ +import * as vscode from 'vscode'; +import * as fs from "fs"; + +import { ChatContextManager } from '../context/contextManager'; +import { MessageHandler } from './messageHandler'; +import { regInMessage, regOutMessage } from '../util/reg_messages'; +import { logger } from "../util/logger"; + + +regInMessage({command: 'addContext', selected: ''}); +regOutMessage({command: 'appendContext', context: ''}); +export async function addConext(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { + const contextStrList = await ChatContextManager.getInstance().handleContextSelected(message.selected); + for (const contextStr of contextStrList) { + MessageHandler.sendMessage(panel, { command: 'appendContext', context: contextStr }); + } +} + +regInMessage({ command: 'contextDetail', file: '' }); +regOutMessage({ command: 'contextDetailResponse', file: '', result: '' }); +// message: { command: 'contextDetail', file: string } +// read detail context information from file +// return json string +export async function getContextDetail(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { + try { + const fileContent = fs.readFileSync(message.file, 'utf-8'); + MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file': message.file, result: fileContent }); + } catch (error) { + logger.channel()?.error(`Error reading file ${message.file}: ${error}`); + } +} + + diff --git a/src/handler/convertCommand.ts b/src/handler/convertCommand.ts deleted file mode 100644 index fb37362..0000000 --- a/src/handler/convertCommand.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as vscode from 'vscode'; -import CommandManager from '../command/commandManager'; -import { MessageHandler } from './messageHandler'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; - - -regInMessage({command: 'convertCommand', text: ''}); -regOutMessage({command: 'convertCommand', result: ''}); -export async function convertCommand(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - const newText = await CommandManager.getInstance().processText(message.text); - MessageHandler.sendMessage(panel, { command: 'convertCommand', result: newText }); - return; -} - - diff --git a/src/handler/showDiff.ts b/src/handler/diffHandler.ts similarity index 89% rename from src/handler/showDiff.ts rename to src/handler/diffHandler.ts index aac5d39..9f54b0c 100644 --- a/src/handler/showDiff.ts +++ b/src/handler/diffHandler.ts @@ -72,7 +72,7 @@ export async function diffView(code: string, tarFile: string) { vscode.commands.executeCommand('vscode.diff', vscode.Uri.file(curFile), vscode.Uri.file(tempFile), 'Diff View'); } -async function getFileContent(fileName: string): Promise { +export async function getFileContent(fileName: string): Promise { const readFile = util.promisify(fs.readFile); try { // Read file content from fileName @@ -122,7 +122,7 @@ async function getNewCode(message: any): Promise { } regInMessage({ command: 'show_diff', content: '', fileName: '' }); -export async function showDiff(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { +export async function applyCodeWithDiff(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { const newCode = await getNewCode(message); if (!newCode) { return; @@ -131,16 +131,3 @@ export async function showDiff(message: any, panel: vscode.WebviewPanel | vscode diffView(newCode, message.fileName); return; } - -regInMessage({ command: 'block_apply', content: '', fileName: '' }); -export async function blockApply(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { - const newCode = await getNewCode(message); - if (!newCode) { - return; - } - - diffView(newCode, message.fileName); - return; -} - - diff --git a/src/handler/featureToggle.ts b/src/handler/featureToggleHandler.ts similarity index 89% rename from src/handler/featureToggle.ts rename to src/handler/featureToggleHandler.ts index 164eb36..bb21770 100644 --- a/src/handler/featureToggle.ts +++ b/src/handler/featureToggleHandler.ts @@ -17,7 +17,7 @@ export async function featureToggle(message: any, panel: vscode.WebviewPanel|vsc regInMessage({command: 'featureToggles'}); regOutMessage({command: 'featureToggles', features: {'feature name': true}}); -export async function featureToggles(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { +export async function getFeatureToggles(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { const featureTaggles = FTs(); MessageHandler.sendMessage(panel, {command: 'featureToggles', features: featureTaggles}); } \ No newline at end of file diff --git a/src/handler/loadHandlers.ts b/src/handler/handlerRegister.ts similarity index 52% rename from src/handler/loadHandlers.ts rename to src/handler/handlerRegister.ts index f38e2bf..c9092a3 100644 --- a/src/handler/loadHandlers.ts +++ b/src/handler/handlerRegister.ts @@ -1,25 +1,20 @@ import { messageHandler } from './messageHandler'; -import { codeApply } from './codeApply'; -import { codeFileApply } from './codeFileApply'; -import { convertCommand } from './convertCommand'; -import { doCommit } from './doCommit'; -import { historyMessages } from './historyMessages'; -import { regCommandList, regCommandListByDevChatRun } from './regCommandList'; -import { regContextList } from './regContextList'; +import { insertCodeBlockToFile } from './codeBlockHandler'; +import { replaceCodeBlockToFile } from './codeBlockHandler'; +import { doCommit } from './commitHandler'; +import { getHistoryMessages } from './historyMessagesHandler'; +import { getWorkflowCommandList } from './workflowCommandHandler'; +import { getWorkflowContextList } from './workflowContextHandler'; import { sendMessage, stopDevChat, regeneration, deleteChatMessage, userInput } from './sendMessage'; -import { blockApply } from './showDiff'; -import { showDiff } from './showDiff'; -import { addConext } from './addContext'; -import { addRefCommandContext } from './addRefCommandContext'; -import { contextDetail } from './contextDetail'; +import { applyCodeWithDiff } from './diffHandler'; +import { addConext } from './contextHandler'; +import { getContextDetail } from './contextHandler'; import { listAllMessages } from './listMessages'; -import { regActionList } from './regActionList'; -import { applyAction } from './applyAction'; -import { doCommand } from './doCommand'; -import { getSetting, updateSetting } from './updateConfig'; -import { featureToggle, featureToggles } from './featureToggle'; -import { getUserAccessKey } from './userAccessKey'; -import { regModelList } from './regValidModelList'; +import { doVscodeCommand } from './vscodeCommandHandler'; +import { getSetting, updateSetting } from './userSettingHandler'; +import { featureToggle, getFeatureToggles } from './featureToggleHandler'; +import { getUserAccessKey } from './accessKeyHandler'; +import { getValidLlmModelList } from './llmModelHandler'; // According to the context menu selected by the user, add the corresponding context file @@ -27,26 +22,23 @@ import { regModelList } from './regValidModelList'; messageHandler.registerHandler('addContext', addConext); // Apply the code block replied by AI to the currently active view // Response: none -messageHandler.registerHandler('code_apply', codeApply); +messageHandler.registerHandler('code_apply', insertCodeBlockToFile); // Apply the code block replied by AI to the currently active view, replacing the current file content // Response: none -messageHandler.registerHandler('code_file_apply', codeFileApply); -// Convert the command input into a natural language description sent to AI -// Response: { command: 'convertCommand', result: } -messageHandler.registerHandler('convertCommand', convertCommand); +messageHandler.registerHandler('code_file_apply', replaceCodeBlockToFile); // Perform commit operation // Response: none messageHandler.registerHandler('doCommit', doCommit); // Get the history messages, called when the user view is displayed // Response: { command: 'historyMessages', result: } // is a list, the specific attribute information is determined when the interface is added -messageHandler.registerHandler('historyMessages', historyMessages); +messageHandler.registerHandler('historyMessages', getHistoryMessages); // Register the command list // Response: { command: 'regCommandList', result: } -messageHandler.registerHandler('regCommandList', regCommandListByDevChatRun); +messageHandler.registerHandler('regCommandList', getWorkflowCommandList); // Register the context list // Response: { command: 'regContextList', result: } -messageHandler.registerHandler('regContextList', regContextList); +messageHandler.registerHandler('regContextList', getWorkflowContextList); // Send a message, send the message entered by the user to AI // Response: // { command: 'receiveMessagePartial', text: , user: , date: } @@ -57,42 +49,32 @@ messageHandler.registerHandler('sendMessage', sendMessage); messageHandler.registerHandler('stopDevChat', stopDevChat); // Show diff // Response: none -messageHandler.registerHandler('block_apply', blockApply); // Show diff, for historical reasons, the same as above -messageHandler.registerHandler('show_diff', showDiff); -// Process the ref command entered by the user -// Response: { command: 'appendContext', context: } -messageHandler.registerHandler('addRefCommandContext', addRefCommandContext); +messageHandler.registerHandler('show_diff', applyCodeWithDiff); // Get context details // Response: { command: 'contextDetailResponse', 'file':, result: } // is a JSON string -messageHandler.registerHandler('contextDetail', contextDetail); +messageHandler.registerHandler('contextDetail', getContextDetail); // Debug handler messageHandler.registerHandler('listAllMessages', listAllMessages); // Regeneration // The response is the same as sendMessage messageHandler.registerHandler('regeneration', regeneration); -// Register the action list -// Response: { command: 'regActionList', result: } -messageHandler.registerHandler('regActionList', regActionList); -// Apply action for code block -// Response: none -messageHandler.registerHandler('applyAction', applyAction); // Delete chat message // Response: { command: 'deletedChatMessage', result: } messageHandler.registerHandler('deleteChatMessage', deleteChatMessage); // Execute vscode command // Response: none -messageHandler.registerHandler('doCommand', doCommand); +messageHandler.registerHandler('doCommand', doVscodeCommand); messageHandler.registerHandler('updateSetting', updateSetting); messageHandler.registerHandler('getSetting', getSetting); messageHandler.registerHandler('featureToggle', featureToggle); -messageHandler.registerHandler('featureToggles', featureToggles); +messageHandler.registerHandler('featureToggles', getFeatureToggles); messageHandler.registerHandler('getUserAccessKey', getUserAccessKey); -messageHandler.registerHandler('regModelList', regModelList); +messageHandler.registerHandler('regModelList', getValidLlmModelList); messageHandler.registerHandler('userInput', userInput); diff --git a/src/handler/historyMessages.ts b/src/handler/historyMessagesHandler.ts similarity index 91% rename from src/handler/historyMessages.ts rename to src/handler/historyMessagesHandler.ts index c0e805d..886dd3e 100644 --- a/src/handler/historyMessages.ts +++ b/src/handler/historyMessagesHandler.ts @@ -10,7 +10,7 @@ import { UiUtilWrapper } from '../util/uiUtil'; regInMessage({command: 'historyMessages', page: 0}); regOutMessage({command: 'loadHistoryMessages', entries: [{hash: '',user: '',date: '',request: '',response: '',context: [{content: '',role: ''}]}]}); -export async function historyMessages(message: {command: string, page: number}, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { +export async function getHistoryMessages(message: {command: string, page: number}, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { // if history message has load, send it to webview const maxCount = Number(UiUtilWrapper.getConfiguration('DevChat', 'maxLogCount')); const skip = maxCount * (message.page ? message.page : 0); diff --git a/src/handler/regValidModelList.ts b/src/handler/llmModelHandler.ts similarity index 73% rename from src/handler/regValidModelList.ts rename to src/handler/llmModelHandler.ts index b00d065..a5fe244 100644 --- a/src/handler/regValidModelList.ts +++ b/src/handler/llmModelHandler.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import ChatContextManager from '../context/contextManager'; +import { ChatContextManager } from '../context/contextManager'; import { MessageHandler } from './messageHandler'; import { regInMessage, regOutMessage } from '../util/reg_messages'; import { ApiKeyManager } from '../util/apiKey'; @@ -8,7 +8,7 @@ import { UiUtilWrapper } from '../util/uiUtil'; regInMessage({command: 'regModelList'}); regOutMessage({command: 'regModelList', result: [{name: ''}]}); -export async function regModelList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { +export async function getValidLlmModelList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { const modelList = await ApiKeyManager.getValidModels(); MessageHandler.sendMessage(panel, { command: 'regModelList', result: modelList }); diff --git a/src/handler/messageHandler.ts b/src/handler/messageHandler.ts index e1c83f8..d75328d 100644 --- a/src/handler/messageHandler.ts +++ b/src/handler/messageHandler.ts @@ -2,18 +2,12 @@ import * as vscode from 'vscode'; -import '../command/loadCommands'; import '../context/loadContexts'; import { logger } from '../util/logger'; import { isWaitForApiKey } from './historyMessagesBase'; -import { onApiKey } from './historyMessages'; +import { onApiKey } from './historyMessagesHandler'; import { ApiKeyManager } from '../util/apiKey'; -import { regeneration, sendMessage as sendMessageX } from './sendMessage'; -import { codeFileApply } from './codeFileApply'; -import { applyAction } from './applyAction'; -import { FT } from '../util/feature_flags/feature_toggles'; -let autox = false; export class MessageHandler { private handlers: { [command: string]: (message: any, panel: vscode.WebviewPanel|vscode.WebviewView) => Promise } = {}; @@ -50,14 +44,6 @@ export class MessageHandler { } } - autox = false; - if (message.command === 'sendMessage') { - // if "/autox" in message.text, then flag global variable autox to true - if (message.text.indexOf('/autox') !== -1) { - autox = true; - } - } - const handler = this.handlers[message.command]; if (handler) { logger.channel()?.info(`Handling the command "${message.command}"`); @@ -79,54 +65,6 @@ export class MessageHandler { } panel.webview.postMessage(message); - - if (message.command === 'receiveMessage') { - // if message.isError is true, then regenerate message - if (message.isError) { - if (autox) { - regeneration({}, panel); - } - } else { - // if message.text is ```command\n {xxx} ``` then get xxx - const messageText = message.text; - // if messageText match "```command\n ... ```", then parse block content - const reg = /```command\n([\s\S]*)```/; - const match = messageText.match(reg); - if (match) { - const command = match[1]; - try { - const commandObject = JSON.parse(command); - if (!(commandObject && commandObject.name)) { - logger.channel()?.error(`${command} is not a valid command`); - logger.channel()?.show(); - return ; - } - - if (commandObject.name === "fail_task" || commandObject.name === "finish_task") { - logger.channel()?.info(`Task has finished.`); - logger.channel()?.show(); - return ; - } - - applyAction({"content": command, "fileName": "", parentHash: message.hash}, panel); - - } catch (e) { - logger.channel()?.error(`parse ${command} error: ${e}`); - logger.channel()?.show(); - - if (autox) { - MessageHandler.sendMessage(panel, { "command": "systemMessage", "text": "continue. 并且确认你在围绕最初的任务在执行相关步骤。" }); - sendMessageX({command: 'sendMessage', text: "continue"}, panel); - } - } - } else { - if (autox) { - MessageHandler.sendMessage(panel, { "command": "systemMessage", "text": "continue. 并且确认你在围绕最初的任务在执行相关步骤。" }); - sendMessageX({command: 'sendMessage', text: "continue"}, panel); - } - } - } - } } } diff --git a/src/handler/regActionList.ts b/src/handler/regActionList.ts deleted file mode 100644 index 918ecf8..0000000 --- a/src/handler/regActionList.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as vscode from 'vscode'; -import { MessageHandler } from './messageHandler'; -import { regInMessage, regOutMessage } from '../util/reg_messages'; -import ActionManager from '../action/actionManager'; - - - -regInMessage({command: 'regActionList'}); -regOutMessage({command: 'regActionList', result: [{name: '', description: '', type: ['', ''], action: ''}]}); -export async function regActionList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - const actionList = ActionManager.getInstance().getActionList(); - MessageHandler.sendMessage(panel, { command: 'regActionList', result: actionList }); -} - - diff --git a/src/handler/sendMessage.ts b/src/handler/sendMessage.ts index 26fa659..6bf2360 100644 --- a/src/handler/sendMessage.ts +++ b/src/handler/sendMessage.ts @@ -2,22 +2,17 @@ import * as vscode from 'vscode'; import { MessageHandler } from './messageHandler'; import { regInMessage, regOutMessage } from '../util/reg_messages'; -import { stopDevChatBase, sendMessageBase, deleteChatMessageBase, insertDevChatLog, handleTopic } from './sendMessageBase'; +import { + stopDevChatBase, + sendMessageBase, + deleteChatMessageBase, + sendTextToDevChat + } from './sendMessageBase'; import { UiUtilWrapper } from '../util/uiUtil'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { ApiKeyManager } from '../util/apiKey'; -import { logger } from '../util/logger'; -import { exec as execCb } from 'child_process'; -import { promisify } from 'util'; -import { CommandResult, CommandRun, createTempSubdirectory } from '../util/commonUtil'; -import { WorkflowRunner } from './workflowExecutor'; -import DevChat from '../toolwrapper/devchat'; -const exec = promisify(execCb); - -let commandRunner : WorkflowRunner | null = null; let _lastMessage: any = undefined; @@ -37,68 +32,11 @@ export function deleteTempFiles(fileName: string): void { fs.unlinkSync(fileName); } -regInMessage({command: 'userInput', text: '{"field": "value", "field2": "value2"}'});; -export async function userInput(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - commandRunner?.input(message.text); -} +function writeContextInfoToTempFiles(message : any) : string[] { + let tempFiles: string[] = []; + message.old_text = message.old_text || message.text; - -// eslint-disable-next-line @typescript-eslint/naming-convention -regInMessage({command: 'sendMessage', text: '', parent_hash: undefined}); -regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'}); -regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', date: 'xxx'}); -export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView, functionName: string|undefined = undefined): Promise { - // check whether the message is a command - if (functionName !== undefined && functionName !== "") { - const messageText = _lastMessage[0].text.trim(); - if (messageText[0] === '/' && message.text[0] !== '/') { - const indexS = messageText.indexOf(' '); - let preCommand = messageText; - if (indexS !== -1) { - preCommand = messageText.substring(0, indexS); - } - - message.text = preCommand + ' ' + message.text; - } - } - _lastMessage = [message, functionName]; - - const messageText = message.text.trim(); - if (messageText[0] === '/') { - // split messageText by ' ' or '\n' or '\t' - const messageTextArr = messageText.split(/ |\n|\t/); - // get command name from messageTextArr - const commandName = messageTextArr[0].substring(1); - // test whether the command is a execute command - const devChat = new DevChat(); - const stdout = await devChat.commandPrompt(commandName); - // try parse stdout by json - let stdoutJson: any = null; - try { - stdoutJson = JSON.parse(stdout); - } catch (error) { - // do nothing - } - - if (stdoutJson) { - // run command - try { - commandRunner = null; - - commandRunner = new WorkflowRunner(); - await commandRunner.run(commandName, stdoutJson, message, panel); - } finally { - commandRunner = null; - } - - return ; - } - } - - // Add a new field to store the names of temporary files - let tempFiles: string[] = []; - - // Handle the contextInfo field in the message + // Handle the contextInfo field in the message if (Array.isArray(message.contextInfo)) { for (let context of message.contextInfo) { if (typeof context === 'object' && context !== null && 'context' in context) { @@ -115,16 +53,34 @@ export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscod } } // Insert the file name into the text field - message.text += ` [context|${context.file}]`; + message.text = message.old_text + ` [context|${context.file}]`; } } } - // clear message.contextInfo - message.contextInfo = undefined; + + return tempFiles; +} + +regInMessage({command: 'userInput', text: '{"field": "value", "field2": "value2"}'});; +export async function userInput(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { + sendTextToDevChat(message.text); +} + + +// eslint-disable-next-line @typescript-eslint/naming-convention +regInMessage({command: 'sendMessage', text: '', parent_hash: undefined}); +regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'}); +regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', date: 'xxx'}); +export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView, functionName: string|undefined = undefined): Promise { + // check whether the message is a command + _lastMessage = message; + + // Add a new field to store the names of temporary files + const tempFiles: string[] = writeContextInfoToTempFiles(message); const responseMessage = await sendMessageBase(message, (data: { command: string, text: string, user: string, date: string}) => { MessageHandler.sendMessage(panel, data, false); - }, functionName); + }); if (responseMessage) { MessageHandler.sendMessage(panel, responseMessage); } @@ -141,18 +97,13 @@ regInMessage({command: 'regeneration'}); export async function regeneration(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { // call sendMessage to send last message again if (_lastMessage) { - await sendMessage(_lastMessage[0], panel, _lastMessage[1]); + await sendMessage(_lastMessage, panel); } } regInMessage({command: 'stopDevChat'}); export async function stopDevChat(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { stopDevChatBase(message); - - if (commandRunner) { - commandRunner.stop(); - commandRunner = null; - } } regInMessage({command: 'deleteChatMessage', hash: 'xxx'}); diff --git a/src/handler/sendMessageBase.ts b/src/handler/sendMessageBase.ts index 472c427..deadb23 100644 --- a/src/handler/sendMessageBase.ts +++ b/src/handler/sendMessageBase.ts @@ -1,14 +1,100 @@ -import DevChat, { ChatResponse } from '../toolwrapper/devchat'; -import CommandManager from '../command/commandManager'; +import DevChat, { ChatOptions, ChatResponse } from '../toolwrapper/devchat'; import { logger } from '../util/logger'; import messageHistory from '../util/messageHistory'; import { TopicManager } from '../topic/topicManager'; -import CustomCommands from '../command/customCommand'; +import { assertValue } from '../util/check'; -let waitCreateTopic = false; +/** + * Class to handle topic updates. + */ +export class TopicUpdateHandler { + /** + * Flag to indicate if the topic is being updated from the webview. + */ + private static isTopicUpdatingFromWebview: boolean = false; + /** + * Checks if the topic change was triggered by the webview. + * + * @returns {boolean} - Returns true if the topic change was triggered by the webview, false otherwise. + */ + public static isTopicChangeTriggeredByWebview(): boolean { + return TopicUpdateHandler.isTopicUpdatingFromWebview; + } + /** + * Processes the topic change after a chat. + * + * @param {string | undefined} parentHash - The hash of the parent message, if any. + * @param {any} message - The message object. + * @param {ChatResponse} chatResponse - The chat response object. + * @returns {Promise} - A Promise that resolves when the topic change has been processed. + */ + public static async processTopicChangeAfterChat(parentHash:string | undefined, message: any, chatResponse: ChatResponse): Promise { + TopicUpdateHandler.isTopicUpdatingFromWebview = true; + try { + if (!chatResponse.isError) { + let topicId = TopicManager.getInstance().currentTopicId; + if (!topicId) { + // create new topic + const topic = TopicManager.getInstance().createTopic(); + topicId = topic.topicId; + } + + TopicManager.getInstance().updateTopic( + topicId!, + chatResponse['prompt-hash'], + parseDateStringToTimestamp(chatResponse.date), + message.text, + chatResponse.response + ); + } + } finally { + TopicUpdateHandler.isTopicUpdatingFromWebview = false; + } + } +} + +/** + * Class to handle user interaction stop events. + */ +class UserStopHandler { + /** + * Flag to indicate if user interaction is stopped. + */ + private static userStop: boolean = false; + + /** + * Stops user interaction. + */ + public static stopUserInteraction(): void { + UserStopHandler.userStop = true; + } + + /** + * Resumes user interaction. + */ + public static resumeUserInteraction(): void { + UserStopHandler.userStop = false; + } + + /** + * Checks if user interaction is stopped. + * + * @returns {boolean} - Returns true if user interaction is stopped, false otherwise. + */ + public static isUserInteractionStopped(): boolean { + return UserStopHandler.userStop; + } +} + +/** + * Parses a date string and returns the corresponding timestamp. + * + * @param {string} dateString - The date string to be parsed. + * @returns {number} - The timestamp corresponding to the date string. + */ function parseDateStringToTimestamp(dateString: string): number { const dateS = Date.parse(dateString); if (!isNaN(dateS)) { @@ -24,11 +110,12 @@ function parseDateStringToTimestamp(dateString: string): number { return date.getTime(); } -export function getWaitCreateTopic(): boolean { - return waitCreateTopic; -} - -// Add this function to messageHandler.ts +/** + * Parses a message and extracts the context, instruction, reference, and text. + * + * @param {string} message - The message to be parsed. + * @returns {{ context: string[]; instruction: string[]; reference: string[]; text: string }} - An object containing the context, instruction, reference, and text extracted from the message. + */ export function parseMessage(message: string): { context: string[]; instruction: string[]; reference: string[]; text: string } { const contextRegex = /\[context\|(.*?)\]/g; const instructionRegex = /\[instruction\|(.*?)\]/g; @@ -65,148 +152,151 @@ export function parseMessage(message: string): { context: string[]; instruction: return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text }; } -export function getInstructionFiles(): string[] { - const instructionFiles: string[] = []; +/** + * Parses a message and sets the chat options based on the parsed message. + * + * @param {any} message - The message object. + * @returns {Promise<[{ context: string[]; instruction: string[]; reference: string[]; text: string }, ChatOptions]>} - A Promise that resolves to an array containing the parsed message and the chat options. + */ +export async function parseMessageAndSetOptions(message: any): Promise<[{ context: string[]; instruction: string[]; reference: string[]; text: string }, ChatOptions]> { + const parsedMessage = parseMessage(message.text); - const customCommands = CustomCommands.getInstance().getCommands(); - // visit customCommands, get default command - for (const command of customCommands) { - if (command.default) { - for (const instruction of command.instructions) { - instructionFiles.push(`${instruction}`); - } - } - } + const chatOptions: ChatOptions = { + header: [], + ...message.parent_hash ? { parent: message.parent_hash } : {}, + ...parsedMessage.context.length > 0 ? { context: parsedMessage.context } : {}, + ...parsedMessage.reference.length > 0 ? { reference: parsedMessage.reference } : {}, + }; - return instructionFiles; + return [parsedMessage, chatOptions]; } -const devChat = new DevChat(); -let userStop = false; - - -// 将解析消息的部分提取到一个单独的函数中 -export async function parseMessageAndSetOptions(message: any, chatOptions: any): Promise<{ context: string[]; instruction: string[]; reference: string[]; text: string }> { - const newText2 = await CommandManager.getInstance().processText(message.text); - const parsedMessage = parseMessage(newText2); - - if (parsedMessage.context.length > 0) { - chatOptions.context = parsedMessage.context; - } - - chatOptions.header = getInstructionFiles(); - if ((parsedMessage.instruction && parsedMessage.instruction.length > 0) || newText2 !== message.text) { - chatOptions.header = parsedMessage.instruction; - } - - if (parsedMessage.reference.length > 0) { - chatOptions.reference = parsedMessage.reference; - } - - return parsedMessage; +/** + * Adds a message to the message history. + * + * @param {any} message - The message object. + * @param {ChatResponse} chatResponse - The chat response object. + * @param {string | undefined} parentHash - The hash of the parent message, if any. + * @returns {void} + */ +export async function addMessageToHistory(message: any, chatResponse: ChatResponse, parentHash: string | undefined): Promise { + messageHistory.add({ + request: message.text, + text: chatResponse.response, + parentHash, + hash: chatResponse['prompt-hash'], + user: chatResponse.user, + date: chatResponse.date + }); } - -export async function handleTopic(parentHash:string | undefined, message: any, chatResponse: ChatResponse) { - waitCreateTopic = true; - try { - if (!chatResponse.isError) { - messageHistory.add({ request: message.text, text: chatResponse.response, parentHash, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date }); - - let topicId = TopicManager.getInstance().currentTopicId; - if (!topicId) { - // create new topic - const topic = TopicManager.getInstance().createTopic(); - topicId = topic.topicId; - } - - TopicManager.getInstance().updateTopic(topicId!, chatResponse['prompt-hash'], parseDateStringToTimestamp(chatResponse.date), message.text, chatResponse.response); - } - } finally { - waitCreateTopic = false; - } -} - -export async function handlerResponseText(partialDataText: string, chatResponse: ChatResponse) : Promise { +/** + * Processes the chat response by replacing certain patterns in the response text. + * + * @param {ChatResponse} chatResponse - The chat response object. + * @returns {string} - The processed response text. + */ +export function processChatResponse(chatResponse: ChatResponse) : string { let responseText = chatResponse.response.replace(/```\ncommitmsg/g, "```commitmsg"); - if (userStop) { - userStop = false; - if (chatResponse.isError) { - return undefined; - } - } - return responseText; } -// 重构后的sendMessage函数 -export async function sendMessageBase(message: any, handlePartialData: (data: { command: string, text: string, user: string, date: string}) => void, function_name: string| undefined = undefined): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }|undefined> { - userStop = false; - const chatOptions: any = {}; - const parsedMessage = await parseMessageAndSetOptions(message, chatOptions); - if (message.parent_hash) { - chatOptions.parent = message.parent_hash; +const devChat = new DevChat(); + +/** + * Sends a message to the DevChat and handles the response. + * + * @param {any} message - The message object. + * @param {(data: { command: string, text: string, user: string, date: string}) => void} handlePartialData - A function to handle partial data. + * @param {string | undefined} function_name - The name of the function, if any. + * @returns {Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean } | undefined>} - A Promise that resolves to an object containing the command, text, hash, user, date, and isError properties, or undefined if an error occurred. + */ +export async function sendMessageBase(message: any, handlePartialData: (data: { command: string, text: string, user: string, date: string}) => void): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }|undefined> { + try { + UserStopHandler.resumeUserInteraction(); + + // parse context and others from message + const [parsedMessage, chatOptions] = await parseMessageAndSetOptions(message); + logger.channel()?.info(`parent hash: ${chatOptions.parent}`); + + // call devchat chat + const chatResponse = await devChat.chat( + parsedMessage.text, + chatOptions, + (partialResponse: ChatResponse) => { + const partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg"); + handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date }); + }); + + assertValue(UserStopHandler.isUserInteractionStopped(), "User Stopped"); + + await addMessageToHistory(message, chatResponse, message.parent_hash); + await TopicUpdateHandler.processTopicChangeAfterChat(message.parent_hash, message, chatResponse); + + return { + command: 'receiveMessage', + text: processChatResponse(chatResponse), + hash: chatResponse['prompt-hash'], + user: chatResponse.user, + date: chatResponse.date, + isError: chatResponse.isError + }; + } catch (error: any) { + logger.channel()?.error(`Error occurred while sending response: ${error.message}`); + return ; + } finally { + UserStopHandler.resumeUserInteraction(); } - logger.channel()?.info(`parent hash: ${chatOptions.parent}`); - - chatOptions.functions = "./.chat/functions.json"; - if (function_name) { - chatOptions.function_name = function_name; - chatOptions.role = "function"; - } - - let partialDataText = ''; - const onData = (partialResponse: ChatResponse) => { - partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg"); - handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date }); - }; - - const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData); - await handleTopic(message.parent_hash, message, chatResponse); - const responseText = await handlerResponseText(partialDataText, chatResponse); - if (responseText === undefined) { - return; - } - - return { command: 'receiveMessage', text: responseText, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date, isError: chatResponse.isError }; } +/** + * Stops the DevChat and user interaction. + * + * @param {any} message - The message object. + * @returns {Promise} - A Promise that resolves when the DevChat and user interaction have been stopped. + */ export async function stopDevChatBase(message: any): Promise { logger.channel()?.info(`Stopping devchat`); - userStop = true; + UserStopHandler.stopUserInteraction(); devChat.stop(); } -export async function insertDevChatLog(message: any, request: string, response: string): Promise { - logger.channel()?.info(`Inserting devchat log`); - await devChat.logInsert(request, response, message.parent_hash); - const logs = await devChat.log({"maxCount": 1}); - if (logs && logs.length > 0) { - return logs[0]['hash']; - } else { - return undefined; +/** + * Deletes a chat message. + * Each message is identified by a hash. + * + * @param {Object} message - The message object. + * @param {string} message.hash - The hash of the message. + * @returns {Promise} - Returns true if the deletion was successful, false otherwise. + * @throws Will throw an error if the deletion was unsuccessful. + */ +export async function deleteChatMessageBase(message:{'hash': string}): Promise { + try { + assertValue(!message.hash, 'Message hash is required'); + // delete the message from messageHistory + messageHistory.delete(message.hash); + + // delete the message by devchat + const bSuccess = await devChat.delete(message.hash); + assertValue(!bSuccess, "Failed to delete message from devchat"); + + TopicManager.getInstance().deleteMessageOnCurrentTopic(message.hash); + return true; + } catch (error: any) { + logger.channel()?.error(`Error: ${error.message}`); + logger.channel()?.show(); + return false; } } -// delete a chat message -// each message is identified by hash -export async function deleteChatMessageBase(message:{'hash': string}): Promise { - // if hash is undefined, return - if (!message.hash) { - return true; - } - // delete the message from messageHistory - messageHistory.delete(message.hash); - - // delete the message by devchat - const bSuccess = await devChat.delete(message.hash); - if (bSuccess) { - let topicId = TopicManager.getInstance().currentTopicId; - if (topicId) { - TopicManager.getInstance().deleteMessage(topicId, message.hash); - } - } - return bSuccess; +/** + * Sends a text message to the DevChat. + * + * @param {string} text - The text message to be sent. + * @returns {Promise} - A Promise that resolves when the message has been sent. + */ +export async function sendTextToDevChat(text: string): Promise { + return devChat.input(text); } \ No newline at end of file diff --git a/src/handler/updateConfig.ts b/src/handler/userSettingHandler.ts similarity index 100% rename from src/handler/updateConfig.ts rename to src/handler/userSettingHandler.ts diff --git a/src/handler/doCommand.ts b/src/handler/vscodeCommandHandler.ts similarity index 84% rename from src/handler/doCommand.ts rename to src/handler/vscodeCommandHandler.ts index 2264bab..91b7eec 100644 --- a/src/handler/doCommand.ts +++ b/src/handler/vscodeCommandHandler.ts @@ -7,7 +7,7 @@ import { regInMessage, regOutMessage } from '../util/reg_messages'; import { logger } from '../util/logger'; regInMessage({command: 'doCommand', content: ['command', 'arg1', 'arg2']}); -export async function doCommand(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { +export async function doVscodeCommand(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { // execute vscode command // message.content[0]: vscode command // message.content[1:]: args for command diff --git a/src/handler/regCommandList.ts b/src/handler/workflowCommandHandler.ts similarity index 52% rename from src/handler/regCommandList.ts rename to src/handler/workflowCommandHandler.ts index 5219523..2f35180 100644 --- a/src/handler/regCommandList.ts +++ b/src/handler/workflowCommandHandler.ts @@ -1,35 +1,46 @@ import * as vscode from 'vscode'; -import CommandManager from '../command/commandManager'; import { MessageHandler } from './messageHandler'; import { regInMessage, regOutMessage } from '../util/reg_messages'; import { ApiKeyManager } from '../util/apiKey'; +import DevChat from '../toolwrapper/devchat'; -regInMessage({command: 'regCommandList'}); -regOutMessage({command: 'regCommandList', result: [{name: '', pattern: '', description: ''}]}); -export async function regCommandList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - const commandList = CommandManager.getInstance().getCommandList(); - const commandCovertedList = commandList.map(command => { - if (command.args > 0) { - // replace {{prompt}} with {{["",""]}}, count of "" is args - const prompt = Array.from({length: command.args}, () => ""); - command.pattern = command.pattern.replace('{{prompt}}', '{{' + JSON.stringify(prompt) + '}}'); - } - return command; +export interface Command { + name: string; + pattern: string; + description: string; + args: number; + handler: (commandName: string, userInput: string) => Promise; +} + +async function getCommandListByDevChatRun(includeHide: boolean = false): Promise { + // load commands from CustomCommands + let newCommands: Command[] = []; + + const devChat = new DevChat(); + const commandList = await devChat.commands(); + commandList.forEach(command => { + const commandObj: Command = { + name: command.name, + pattern: command.name, + description: command.description, + args: 0, + handler: async (commandName: string, userInput: string) => { return ''; } + }; + newCommands.push(commandObj); }); - - MessageHandler.sendMessage(panel, { command: 'regCommandList', result: commandCovertedList }); - return; + + return newCommands; } let existPannel: vscode.WebviewPanel|vscode.WebviewView|undefined = undefined; regInMessage({command: 'regCommandList'}); regOutMessage({command: 'regCommandList', result: [{name: '', pattern: '', description: ''}]}); -export async function regCommandListByDevChatRun(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { +export async function getWorkflowCommandList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { existPannel = panel; - const commandList = await CommandManager.getInstance().getCommandListByDevChatRun(); + const commandList = await getCommandListByDevChatRun(); const commandCovertedList = commandList.map(command => { if (command.args > 0) { // replace {{prompt}} with {{["",""]}}, count of "" is args @@ -45,7 +56,7 @@ export async function regCommandListByDevChatRun(message: any, panel: vscode.Web export async function sendCommandListByDevChatRun() { if (existPannel) { - regCommandListByDevChatRun({}, existPannel!); + getWorkflowCommandList({}, existPannel!); } } @@ -53,4 +64,3 @@ export async function updateChatModels() { const modelList = await ApiKeyManager.getValidModels(); MessageHandler.sendMessage(existPannel!, { command: 'regModelList', result: modelList }); } - diff --git a/src/handler/regContextList.ts b/src/handler/workflowContextHandler.ts similarity index 71% rename from src/handler/regContextList.ts rename to src/handler/workflowContextHandler.ts index 61d2a58..a0b2887 100644 --- a/src/handler/regContextList.ts +++ b/src/handler/workflowContextHandler.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import ChatContextManager from '../context/contextManager'; +import { ChatContextManager } from '../context/contextManager'; import { MessageHandler } from './messageHandler'; import { regInMessage, regOutMessage } from '../util/reg_messages'; @@ -7,10 +7,8 @@ import { regInMessage, regOutMessage } from '../util/reg_messages'; regInMessage({command: 'regContextList'}); regOutMessage({command: 'regContextList', result: [{name: '', description: ''}]}); -export async function regContextList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { +export async function getWorkflowContextList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { const contextList = ChatContextManager.getInstance().getContextList(); MessageHandler.sendMessage(panel, { command: 'regContextList', result: contextList }); return; } - - diff --git a/src/handler/workflowExecutor.ts b/src/handler/workflowExecutor.ts deleted file mode 100644 index ec126b7..0000000 --- a/src/handler/workflowExecutor.ts +++ /dev/null @@ -1,283 +0,0 @@ -import * as vscode from 'vscode'; -import { UiUtilWrapper } from "../util/uiUtil"; -import { MessageHandler } from "../handler/messageHandler"; -import { ApiKeyManager } from "../util/apiKey"; -import { logger } from "../util/logger"; -import { CommandResult, CommandRun, saveModelSettings } from "../util/commonUtil"; -import { handleTopic, insertDevChatLog } from "./sendMessageBase"; -import { regInMessage } from "@/util/reg_messages"; -import parseArgsStringToArgv from 'string-argv'; - - -async function handleWorkflowRequest(request): Promise { - /* - request: { - "command": "some command", - "args": { - "arg1": "value1", - "arg2": "value2" - } - } - response: { - "status": "success", - "result": "success", - "detail": "some detail" - } - */ - if (!request || !request.command) { - return undefined; - } - - if (request.command === "get_lsp_brige_port") { - return JSON.stringify({ - "status": "success", - "result": await UiUtilWrapper.getLSPBrigePort() - }); - } else { - return JSON.stringify({ - "status": "fail", - "result": "fail", - "detail": "command is not supported" - }); - } -} - -export class WorkflowRunner { - private _commandRunner: CommandRun | null = null; - private _stop: boolean = false; - private _cacheOut: string = ""; - private _panel: vscode.WebviewPanel|vscode.WebviewView | null = null; - - constructor() {} - - private async _getApiKeyAndApiBase(): Promise<[string | undefined, string | undefined]> { - const llmModelData = await ApiKeyManager.llmModel(); - if (!llmModelData) { - logger.channel()?.error('No valid llm model is selected!'); - logger.channel()?.show(); - return [undefined, undefined]; - } - - let openaiApiKey = llmModelData.api_key; - if (!openaiApiKey) { - logger.channel()?.error('The OpenAI key is invalid!'); - logger.channel()?.show(); - return [undefined, undefined]; - } - - const openAiApiBase = llmModelData.api_base; - return [openaiApiKey, openAiApiBase]; - } - - private _parseCommandOutput(outputStr: string): string { - /* - output is format as: - <> - {"content": "data"} - <> - */ - const outputWitchCache = this._cacheOut + outputStr; - this._cacheOut = ""; - - let outputResult = ""; - let curPos = 0; - while (true) { - const startPos = outputWitchCache.indexOf('<>', curPos); - const startPos2 = outputWitchCache.indexOf('```', curPos); - if (startPos === -1 && startPos2 === -1) { - break; - } - - const isStart = (startPos2 === -1) || (startPos > -1 && startPos < startPos2); - - let endPos = -1; - if (isStart) { - endPos = outputWitchCache.indexOf('<>', startPos+9); - } else { - endPos = outputWitchCache.indexOf('```', startPos2+3); - } - - if (endPos === -1) { - this._cacheOut = outputWitchCache.substring(startPos, outputWitchCache.length); - break; - } - - let contentStr = ""; - if (isStart) { - contentStr = outputWitchCache.substring(startPos+9, endPos); - curPos = endPos+7; - - try { - const contentObj = JSON.parse(contentStr); - if (contentObj && contentObj.result) { - contentStr = contentObj.result; - } - } catch (error) { - } - } else { - contentStr = outputWitchCache.substring(startPos2, endPos+3); - curPos = endPos+3; - } - - outputResult += contentStr.trim() + "\n\n"; - } - - return outputResult; - } - - private _parseLastCommandOutput(outputStr: string): string { - const startPos = outputStr.lastIndexOf('<>'); - const endPos = outputStr.lastIndexOf('<>'); - - if (startPos === -1 || endPos === -1 || endPos < startPos) { - return ''; - } - - return outputStr.substring(startPos+9, endPos); - } - - private async _runCommand(commandWithArgs: string, commandEnvs: any): Promise<[CommandResult | undefined, string]> { - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() || ""; - let commandOutput = ""; - let commandAnswer = ""; - - try { - const commandAndArgsList = parseArgsStringToArgv(commandWithArgs); - this._commandRunner = new CommandRun(); - await saveModelSettings(); - const result = await this._commandRunner.spawnAsync(commandAndArgsList[0], commandAndArgsList.slice(1), { env: commandEnvs, cwd: workspaceDir }, async (data) => { - // handle command stdout - const newData = this._parseCommandOutput(data); - // if newData is json string, then process it by handleWorkflowRequest - let newDataObj: any = undefined; - try { - newDataObj = JSON.parse(newData); - const result = await handleWorkflowRequest(newDataObj); - if (result) { - - this.input(result); - } else if (newDataObj!.result) { - commandAnswer = newDataObj!.result; - commandOutput += newDataObj!.result; - logger.channel()?.info(newDataObj!.result); - MessageHandler.sendMessage(this._panel!, { command: 'receiveMessagePartial', text: commandOutput, hash:"", user:"", isError: false }); - } - } catch (e) { - if (newData.length > 0){ - commandOutput += newData; - logger.channel()?.info(newData); - MessageHandler.sendMessage(this._panel!, { command: 'receiveMessagePartial', text: commandOutput, hash:"", user:"", isError: false }); - } - } - }, (data) => { - // handle command stderr - logger.channel()?.error(data); - logger.channel()?.show(); - }, undefined, undefined); - - - return [result, commandAnswer]; - } catch (error) { - if (error instanceof Error) { - logger.channel()?.error(`error: ${error.message}`); - } else { - logger.channel()?.error(`An unknown error occurred: ${error}`); - } - logger.channel()?.show(); - } - return [undefined, ""]; - } - - public stop(): void { - this._stop = true; - if (this._commandRunner) { - this._commandRunner.stop(); - this._commandRunner = null; - } - } - - public input(data): void { - const userInputWithFlag = `\n<>\n${data}\n<>\n`; - this._commandRunner?.write(userInputWithFlag); - } - - public async run(workflow: string, commandDefines: any, message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { - this._panel = panel; - - // all workflow run in decchat-command virtual env - const pythonVirtualEnv: string | undefined = vscode.workspace.getConfiguration('DevChat').get('PythonForCommands'); - if (!pythonVirtualEnv) { - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: "Python for Commands is not ready, please see OUTPUT for more detail.", hash: "", user: "", date: 0, isError: true }); - return ; - } - - // devchat-core packages are installed in extensionPath/tools/site-packages - const extensionPath = UiUtilWrapper.extensionPath(); - - // api_key and api_base - const [apiKey, aipBase] = await this._getApiKeyAndApiBase(); - if (!apiKey) { - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: "The OpenAI key is invalid!", hash: "", user: "", date: 0, isError: true }); - return ; - } - - // envs for Command - const workflowEnvs = { - // eslint-disable-next-line @typescript-eslint/naming-convention - "PYTHONUTF8":1, - "DEVCHATPYTHON": UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3", - "PYTHONLIBPATH": `${extensionPath}/tools/site-packages`, - "PARENT_HASH": message.parent_hash, - ...process.env, - // eslint-disable-next-line @typescript-eslint/naming-convention - OPENAI_API_KEY: apiKey, - // eslint-disable-next-line @typescript-eslint/naming-convention - ...(aipBase ? { 'OPENAI_API_BASE': aipBase } : {}) - }; - - const requireInput = commandDefines.input === "required"; - if (requireInput && message.text.replace("/" + workflow, "").trim() === "") { - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: `The workflow ${workflow} need input!`, hash: "", user: "", date: 0, isError: true }); - return ; - } - - const workflowCommand = commandDefines.steps[0].run.replace( - '$command_python', `${pythonVirtualEnv}`).replace( - '$input', `${message.text.replace("/" + workflow, "").trim()}`); - - const [commandResult, commandAnswer] = await this._runCommand(workflowCommand, workflowEnvs); - - if (commandResult && commandResult.exitCode === 0) { - const lastOutput = this._parseLastCommandOutput(commandResult.stdout); - let resultOut = commandAnswer === "" ? "success" : commandAnswer; - - try { - const lastOutputObj = JSON.parse(lastOutput); - if (lastOutputObj && lastOutputObj.result) { - resultOut = lastOutputObj.result; - } - } catch (error) {} - - let logHash = await insertDevChatLog(message, message.text, resultOut); - if (!logHash) { - logHash = ""; - logger.channel()?.error(`Failed to insert devchat log.`); - logger.channel()?.show(); - } - - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: resultOut, hash:logHash, user:"", date:0, isError: false }); - - const dateStr = Math.floor(Date.now()/1000).toString(); - await handleTopic( - message.parent_hash, - {"text": message.text}, - // eslint-disable-next-line @typescript-eslint/naming-convention - { response: resultOut, "prompt-hash": logHash, user: "", "date": dateStr, finish_reason: "", isError: false }); - } else if (commandResult) { - logger.channel()?.info(`${commandResult.stdout}`); - if (this._stop === false) { - MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: commandResult.stderr, hash: "", user: "", date: 0, isError: true }); - } - } - } -} diff --git a/src/ide_services/services.ts b/src/ide_services/services.ts new file mode 100644 index 0000000..798fdfb --- /dev/null +++ b/src/ide_services/services.ts @@ -0,0 +1,120 @@ +import * as http from 'http'; +import * as url from 'url'; +import * as querystring from 'querystring'; +import { logger } from '../util/logger'; +import { UiUtilWrapper } from '../util/uiUtil'; + + +const functionRegistry: any = { + "/hellox": { + "keys": [], + "handler": async () => { + return "111222"; + } + }, + "/hellox2": { + "keys": ["a", "b"], + "handler": async (a: string, b: string) => { + return a+b; + } + }, + "/hellox3": { + "keys": [], + "handler": async () => { + return { + "name": "v1", + "age": 20, + "others": { + "address": "sh", + "phone": "123456789" + } + }; + } + }, + "/get_lsp_brige_port": { + "keys": [], + "handler": async () => { + return await UiUtilWrapper.getLSPBrigePort(); + } + } +}; + + + +let server: http.Server | null = null; +export async function startRpcServer() { + server = http.createServer((req, res) => { + const parsedUrl = new URL(req.url!, `http://${req.headers.host}`); + if (parsedUrl.pathname === '/favicon.ico') { + res.writeHead(204); + res.end(); + return; + } + + let params: any = {}; + + if (req.method === 'POST') { + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); // 将Buffer转换为string + }); + + req.on('end', () => { + // 根据不同Content-Type,进行不同方式的解析 + if (req.headers['content-type'] === 'application/json') { + // 解析JSON格式的数据 + params = JSON.parse(body); + + // 处理postParams + } else if (req.headers['content-type'] === 'application/x-www-form-urlencoded') { + // 解析URL编码的数据 + params = querystring.parse(body); + // 处理postParams + } + + handleRequest(parsedUrl, params, res); + }); + } else if (req.method === 'GET') { + const queryParams = parsedUrl.searchParams; + for (let param of queryParams) { + params[param[0]] = param[1]; + } + + handleRequest(parsedUrl, params, res); + } + }); + + async function handleRequest(parsedUrl: URL, params: any, res: http.ServerResponse) { + let responseResult = {}; + + if (functionRegistry[parsedUrl.pathname]) { + let keysExist = true; + let newParameters: any[] = []; + for (let key of functionRegistry[parsedUrl.pathname]['keys']) { + if (!params.hasOwnProperty(key)) { + keysExist = false; + break; + } + newParameters.push(params[key]); + } + if (!keysExist) { + responseResult['error'] = "Missing required parameters"; + } else { + responseResult['result'] = await functionRegistry[parsedUrl.pathname]['handler'](...newParameters); + } + } else { + responseResult['error'] = "Function not found"; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(responseResult)); + } + + server.listen(3000, () => { + const address = server!.address(); + // `address()`返回的对象包含`port`属性,它是系统分配的端口号 + const port = typeof address === 'string' ? address : address?.port; + logger.channel()?.info(`Server running at http://localhost:${port}/`); + process.env.DEVCHAT_IDE_SERVICE_URL = `http://localhost:${port}/`; + }); +} \ No newline at end of file diff --git a/src/panel/chatPanel.ts b/src/panel/chatPanel.ts index 6271587..9472e6c 100644 --- a/src/panel/chatPanel.ts +++ b/src/panel/chatPanel.ts @@ -2,12 +2,10 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import '../handler/loadHandlers'; +import '../handler/handlerRegister'; import handleMessage from '../handler/messageHandler'; import WebviewManager from './webviewManager'; -import CustomCommands from '../command/customCommand'; -import CommandManager from '../command/commandManager'; import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig'; import { UiUtilWrapper } from '../util/uiUtil'; @@ -24,7 +22,6 @@ export default class ChatPanel { const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); if (workspaceDir) { const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows'); - CustomCommands.getInstance().parseCommands(workflowsDir); } if (ChatPanel._instance) { diff --git a/src/panel/devchatView.ts b/src/panel/devchatView.ts index 01e9ff2..54b8e3d 100644 --- a/src/panel/devchatView.ts +++ b/src/panel/devchatView.ts @@ -2,17 +2,14 @@ import * as vscode from 'vscode'; import * as path from 'path'; import WebviewManager from './webviewManager'; -import '../handler/loadHandlers'; +import '../handler/handlerRegister'; import handleMessage from '../handler/messageHandler'; import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig'; -import ExtensionContextHolder from '../util/extensionContext'; -import CustomCommands from '../command/customCommand'; +import { ExtensionContextHolder } from '../util/extensionContext'; import { TopicManager } from '../topic/topicManager'; import { UiUtilWrapper } from '../util/uiUtil'; -import ChatContextManager from '../context/contextManager'; -import ActionManager from '../action/actionManager'; -import { getWaitCreateTopic } from '../handler/sendMessageBase'; -import { CustomActions } from '../action/customAction'; +import { ChatContextManager } from '../context/contextManager'; +import { TopicUpdateHandler } from '../handler/sendMessageBase'; export class DevChatViewProvider implements vscode.WebviewViewProvider { @@ -33,10 +30,6 @@ export class DevChatViewProvider implements vscode.WebviewViewProvider { if (workspaceDir) { const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows'); ChatContextManager.getInstance().loadCustomContexts(workflowsDir); - ActionManager.getInstance().loadCustomActions(workflowsDir); - - const actionInstrucFile = path.join(UiUtilWrapper.workspaceFoldersFirstPath()!, './.chat/functions.json'); - ActionManager.getInstance().saveActionInstructionFile( actionInstrucFile); } } @@ -73,7 +66,7 @@ export class DevChatViewProvider implements vscode.WebviewViewProvider { ); TopicManager.getInstance().addOnCurrentTopicIdChangeListener((topicId) => { - if (topicId && getWaitCreateTopic()) { + if (topicId && TopicUpdateHandler.isTopicChangeTriggeredByWebview()) { return; } this.reloadWebview(); diff --git a/src/panel/statusBarView.ts b/src/panel/statusBarView.ts index 333a409..1dc744e 100644 --- a/src/panel/statusBarView.ts +++ b/src/panel/statusBarView.ts @@ -1,9 +1,8 @@ import * as vscode from 'vscode'; import { dependencyCheck } from './statusBarViewBase'; -import { isIndexingStopped, isNeedIndexingCode } from '../util/askCodeUtil'; import { ProgressBar } from '../util/progressBar'; -import ExtensionContextHolder from '../util/extensionContext'; +import { ExtensionContextHolder } from '../util/extensionContext'; export function createStatusBarItem(context: vscode.ExtensionContext): vscode.StatusBarItem { diff --git a/src/toolwrapper/devchat.ts b/src/toolwrapper/devchat.ts index d1a7031..6545f7c 100644 --- a/src/toolwrapper/devchat.ts +++ b/src/toolwrapper/devchat.ts @@ -1,14 +1,12 @@ -// devchat.ts import * as dotenv from 'dotenv'; import * as path from 'path'; -import * as fs from 'fs'; import { logger } from '../util/logger'; import { CommandRun, saveModelSettings } from "../util/commonUtil"; -import ExtensionContextHolder from '../util/extensionContext'; import { UiUtilWrapper } from '../util/uiUtil'; import { ApiKeyManager } from '../util/apiKey'; -import { exitCode } from 'process'; +import { assertValue } from '../util/check'; +import { getFileContent } from '../handler/diffHandler'; const envPath = path.join(__dirname, '..', '.env'); @@ -18,9 +16,6 @@ export interface ChatOptions { parent?: string; reference?: string[]; header?: string[]; - functions?: string; - role?: string; - function_name?: string; context?: string[]; } @@ -48,29 +43,22 @@ export interface CommandEntry { description: string; } -// define TopicEntry interface -/* -[ - { - root_prompt: LogEntry, - latest_time: 1689849274, - hidden: false, - title: null - } -] -*/ export interface TopicEntry { + // eslint-disable-next-line @typescript-eslint/naming-convention root_prompt: LogEntry; + // eslint-disable-next-line @typescript-eslint/naming-convention latest_time: number; hidden: boolean; title: string | null; } export interface ChatResponse { + // eslint-disable-next-line @typescript-eslint/naming-convention "prompt-hash": string; user: string; date: string; response: string; + // eslint-disable-next-line @typescript-eslint/naming-convention finish_reason: string; isError: boolean; } @@ -83,12 +71,25 @@ class DevChat { this.commandRun = new CommandRun(); } - public stop() { - this.commandRun.stop(); + private async loadContextsFromFiles(contexts: string[] | undefined): Promise { + if (!contexts) { + return []; + } + + const loadedContexts: string[] = []; + for (const context of contexts) { + const contextContent = await getFileContent(context); + if (!contextContent) { + continue; + } + + loadedContexts.push(contextContent); + } + return loadedContexts; } - async buildArgs(options: ChatOptions): Promise { - let args = ["prompt"]; + private async buildArgs(options: ChatOptions): Promise { + let args = ["-m", "devchat", "prompt"]; if (options.reference) { for (const reference of options.reference) { @@ -106,481 +107,22 @@ class DevChat { } } - // TODO: fix openai function calling - // const isEnableFunctionCalling = UiUtilWrapper.getConfiguration('DevChat', 'EnableFunctionCalling'); - // if (options.functions && isEnableFunctionCalling) { - // args.push("-f", options.functions); - // } - - if (options.function_name) { - args.push("-n", options.function_name); - } - if (options.parent) { args.push("-p", options.parent); } const llmModelData = await ApiKeyManager.llmModel(); - if (llmModelData && llmModelData.model) { - args.push("-m", llmModelData.model); - } + assertValue(!llmModelData || !llmModelData.model, 'You must select a LLM model to use for conversations'); + args.push("-m", llmModelData.model); + + args.push("-ns"); + args.push("-a"); return args; } - private parseOutData(stdout: string, isPartial: boolean): ChatResponse { - const responseLines = stdout.trim().split("\n"); - - if (responseLines.length < 2) { - return { - "prompt-hash": "", - user: "", - date: "", - response: "", - finish_reason: "", - isError: isPartial ? false : true, - }; - } - - const userLine = responseLines.shift()!; - const user = (userLine.match(/User: (.+)/)?.[1]) ?? ""; - - const dateLine = responseLines.shift()!; - const date = (dateLine.match(/Date: (.+)/)?.[1]) ?? ""; - - - let promptHashLine = ""; - for (let i = responseLines.length - 1; i >= 0; i--) { - if (responseLines[i].startsWith("prompt")) { - promptHashLine = responseLines[i]; - responseLines.splice(i, 1); - break; - } - } - - let finishReasonLine = ""; - for (let i = responseLines.length - 1; i >= 0; i--) { - if (responseLines[i].startsWith("finish_reason:")) { - finishReasonLine = responseLines[i]; - responseLines.splice(i, 1); - break; - } - } - - if (!promptHashLine) { - return { - "prompt-hash": "", - user: user, - date: date, - response: responseLines.join("\n"), - finish_reason: "", - isError: isPartial ? false : true, - }; - } - - const finishReason = finishReasonLine.split(" ")[1]; - const promptHash = promptHashLine.split(" ")[1]; - const response = responseLines.join("\n"); - - return { - "prompt-hash": promptHash, - user, - date, - response, - finish_reason: finishReason, - isError: false, - }; - } - - async chat(content: string, options: ChatOptions = {}, onData: (data: ChatResponse) => void): Promise { - const llmModelData = await ApiKeyManager.llmModel(); - if (!llmModelData) { - return { - "prompt-hash": "", - user: "", - date: "", - response: `Error: no valid llm model is selected!`, - finish_reason: "", - isError: true, - }; - } - - const args = await this.buildArgs(options); - args.push("--"); - args.push(content); - - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - let openaiApiKey = await ApiKeyManager.getApiKey(); - if (!openaiApiKey) { - logger.channel()?.error('The OpenAI key is invalid!'); - logger.channel()?.show(); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const openAiApiBaseObject = llmModelData.api_base? { OPENAI_API_BASE: llmModelData.api_base } : {}; - const activeLlmModelKey = llmModelData.api_key; - - let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath'); - if (!devChat) { - devChat = 'devchat'; - } - - await saveModelSettings(); - - try { - - let receviedStdout = ""; - const onStdoutPartial = (stdout: string) => { - receviedStdout += stdout; - const data = this.parseOutData(receviedStdout, true); - onData(data); - }; - - const spawnAsyncOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - OPENAI_API_KEY: activeLlmModelKey, - ...openAiApiBaseObject - }, - }; - - // activeLlmModelKey is an api key, I will output it to the log - // so, replace mid-sub string with * - // for example: sk-1234567890 -> sk-1*****7890, keep first 4 char and last 4 char visible - const newActiveLlmModelKey = activeLlmModelKey.replace(/^(.{4})(.*)(.{4})$/, (_, first, middle, last) => first + middle.replace(/./g, '*') + last); - const keyInfo = { - OPENAI_API_KEY: newActiveLlmModelKey , - ...openAiApiBaseObject - }; - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - logger.channel()?.info(`Running devchat with environment: ${JSON.stringify(keyInfo)}`); - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnAsyncOptions, onStdoutPartial, undefined, undefined, undefined); - - if (stderr) { - let newStderr = stderr; - if (stderr.indexOf('Failed to verify access key') > 0) { - newStderr += `\nPlease check your key data: ${JSON.stringify(keyInfo)}`; - } - return { - "prompt-hash": "", - user: "", - date: "", - response: newStderr, - finish_reason: "", - isError: true, - }; - } - - const response = this.parseOutData(stdout, false); - return response; - } catch (error: any) { - return { - "prompt-hash": "", - user: "", - date: "", - response: `Error: ${error.stderr}\nExit code: ${error.code}`, - finish_reason: "", - isError: true, - }; - } - } - - async logInsert(request: string, response: string, parent: string | undefined) { - let log_data = { - "model": "gpt-4", - "messages": [ - { - "role": "user", - "content": request - }, - { - "role": "assistant", - "content": response - } - ], - "timestamp": Math.floor(Date.now()/1000), - "request_tokens": 1, - "response_tokens": 1 - }; - if (parent) { - log_data["parent"] = parent; - } - - - const args = ["log", "--insert", JSON.stringify(log_data)]; - const devChat = this.getDevChatPath(); - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - const openaiApiKey = process.env.OPENAI_API_KEY; - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - const spawnOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - OPENAI_API_KEY: openaiApiKey, - }, - }; - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnOptions, undefined, undefined, undefined, undefined); - - logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); - if (stderr) { - logger.channel()?.error(`Error: ${stderr}`); - logger.channel()?.show(); - return false; - } - if (stdout.indexOf('Failed to insert log') >= 0) { - logger.channel()?.error(`Failed to insert log: ${log_data}`); - logger.channel()?.show(); - return false; - } - - if (code !== 0) { - logger.channel()?.error(`Exit code: ${code}`); - logger.channel()?.show(); - return false; - } - return true; - } - - async delete(hash: string): Promise { - const args = ["log", "--delete", hash]; - const devChat = this.getDevChatPath(); - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - const openaiApiKey = process.env.OPENAI_API_KEY; - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - const spawnOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - OPENAI_API_KEY: openaiApiKey, - }, - }; - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnOptions, undefined, undefined, undefined, undefined); - - logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); - if (stderr) { - logger.channel()?.error(`Error: ${stderr}`); - logger.channel()?.show(); - return false; - } - if (stdout.indexOf('Failed to delete prompt') >= 0) { - logger.channel()?.error(`Failed to delete prompt: ${hash}`); - logger.channel()?.show(); - return false; - } - - if (code !== 0) { - logger.channel()?.error(`Exit code: ${code}`); - logger.channel()?.show(); - return false; - } - return true; - } - - async log(options: LogOptions = {}): Promise { - const args = this.buildLogArgs(options); - const devChat = this.getDevChatPath(); - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - const openaiApiKey = process.env.OPENAI_API_KEY; - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - const spawnOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - OPENAI_API_KEY: openaiApiKey, - }, - }; - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnOptions, undefined, undefined, undefined, undefined); - - logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); - if (stderr) { - logger.channel()?.error(`Error: ${stderr}`); - logger.channel()?.show(); - return []; - } - - const logs = JSON.parse(stdout.trim()).reverse(); - for (const log of logs) { - log.response = log.responses[0]; - delete log.responses; - } - return logs; - } - - // command devchat run --list - // output: - // [ - // { - // "name": "code", - // "description": "Generate code with a general template embedded into the prompt." - // }, - // { - // "name": "code.py", - // "description": "Generate code with a Python-specific template embedded into the prompt." - // }, - // { - // "name": "commit_message", - // "description": "Generate a commit message for the given git diff." - // }, - // { - // "name": "release_note", - // "description": "Generate a release note for the given commit log." - // } - // ] - async commands(): Promise { - const args = ["run", "--list"]; - const devChat = this.getDevChatPath(); - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - const spawnOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - }, - }; - - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnOptions, undefined, undefined, undefined, undefined); - logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); - if (stderr) { - logger.channel()?.error(`Error: ${stderr}`); - logger.channel()?.show(); - return []; - } - - try { - const commands = JSON.parse(stdout.trim()); - return commands; - } catch (error) { - logger.channel()?.error(`Error parsing JSON: ${error}`); - logger.channel()?.show(); - return []; - } - } - - async commandPrompt(command: string): Promise { - const args = ["run", command]; - const devChat = this.getDevChatPath(); - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - const spawnOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - }, - }; - - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnOptions, undefined, undefined, undefined, undefined); - logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); - if (stderr) { - logger.channel()?.error(`Error: ${stderr}`); - logger.channel()?.show(); - } - return stdout; - } - - async updateSysCommand(): Promise { - const args = ["run", "--update-sys"]; - const devChat = this.getDevChatPath(); - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - const spawnOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - }, - }; - - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnOptions, undefined, undefined, undefined, undefined); - logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); - if (stderr) { - logger.channel()?.error(`Error: ${stderr}`); - logger.channel()?.show(); - } - logger.channel()?.info(`${stdout}`); - return stdout; - } - - async topics(): Promise { - const args = ["topic", "-l"]; - const devChat = this.getDevChatPath(); - const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); - - logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); - const spawnOptions = { - maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB - cwd: workspaceDir, - env: { - PYTHONUTF8:1, - PYTHONPATH: UiUtilWrapper.extensionPath() + "/tools/site-packages", - ...process.env, - }, - }; - - const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; - const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, ["-m", "devchat"].concat(args), spawnOptions, undefined, undefined, undefined, undefined); - - logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); - if (stderr) { - logger.channel()?.error(`Error: ${stderr}`); - logger.channel()?.show(); - return []; - } - - try { - const topics = JSON.parse(stdout.trim()).reverse(); - // convert responses to respose, and remove responses field - // responses is in TopicEntry.root_prompt.responses - for (const topic of topics) { - if (topic.root_prompt.responses) { - topic.root_prompt.response = topic.root_prompt.responses[0]; - delete topic.root_prompt.responses; - } - } - return topics; - } catch (error) { - logger.channel()?.error(`Error parsing JSON: ${error}`); - logger.channel()?.show(); - return []; - } - } - private buildLogArgs(options: LogOptions): string[] { - let args = ["log"]; + let args = ["-m", "devchat", "log"]; if (options.skip) { args.push('--skip', `${options.skip}`); @@ -599,12 +141,326 @@ class DevChat { return args; } - private getDevChatPath(): string { - let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath'); - if (!devChat) { - devChat = 'devchat'; + private parseOutData(stdout: string, isPartial: boolean): ChatResponse { + const responseLines = stdout.trim().split("\n"); + + if (responseLines.length < 2) { + return this.createChatResponse("", "", "", "", !isPartial); + } + + const [userLine, remainingLines1] = this.extractLine(responseLines, "User: "); + const user = this.parseLine(userLine, /User: (.+)/); + + const [dateLine, remainingLines2] = this.extractLine(remainingLines1, "Date: "); + const date = this.parseLine(dateLine, /Date: (.+)/); + + const [promptHashLine, remainingLines3] = this.extractLine(remainingLines2, "prompt"); + const [finishReasonLine, remainingLines4] = this.extractLine(remainingLines3, "finish_reason:"); + + if (!promptHashLine) { + return this.createChatResponse("", user, date, remainingLines4.join("\n"), !isPartial); + } + + const finishReason = finishReasonLine.split(" ")[1]; + const promptHash = promptHashLine.split(" ")[1]; + const response = remainingLines4.join("\n"); + + return this.createChatResponse(promptHash, user, date, response, false, finishReason); + } + + private extractLine(lines: string[], startWith: string): [string, string[]] { + const index = lines.findIndex(line => line.startsWith(startWith)); + const extractedLine = index !== -1 ? lines.splice(index, 1)[0] : ""; + return [extractedLine, lines]; + } + + private parseLine(line: string, regex: RegExp): string { + return (line.match(regex)?.[1]) ?? ""; + } + + private createChatResponse(promptHash: string, user: string, date: string, response: string, isError: boolean, finishReason = ""): ChatResponse { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + "prompt-hash": promptHash, + user, + date, + response, + // eslint-disable-next-line @typescript-eslint/naming-convention + finish_reason: finishReason, + isError, + }; + } + + private async runCommand(args: string[]): Promise<{code: number | null, stdout: string, stderr: string}> { + // build env variables for command + const envs = { + // eslint-disable-next-line @typescript-eslint/naming-convention + "PYTHONUTF8":1, + // eslint-disable-next-line @typescript-eslint/naming-convention + "PYTHONPATH": UiUtilWrapper.extensionPath() + "/tools/site-packages", + ...process.env + }; + + const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; + logger.channel()?.info(`Running command:${pythonApp} ${args.join(" ")}`); + + // run command + const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync( + pythonApp, + args, + { + maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB + cwd: UiUtilWrapper.workspaceFoldersFirstPath(), + env: envs + }, + undefined, undefined, undefined, undefined + ); + + return {code, stdout, stderr}; + } + + public input(data: string) { + this.commandRun?.write(data + "\n"); + } + + public stop() { + this.commandRun.stop(); + } + + async chat(content: string, options: ChatOptions = {}, onData: (data: ChatResponse) => void): Promise { + try { + // build args for devchat prompt command + const args = await this.buildArgs(options); + args.push("--"); + args.push(content); + + // build env variables for prompt command + const llmModelData = await ApiKeyManager.llmModel(); + assertValue(!llmModelData, "No valid llm model selected"); + const envs = { + ...process.env, + // eslint-disable-next-line @typescript-eslint/naming-convention + "PYTHONUTF8": 1, + // eslint-disable-next-line @typescript-eslint/naming-convention + "command_python": UiUtilWrapper.getConfiguration('DevChat', 'PythonForCommands') || "python3", + // eslint-disable-next-line @typescript-eslint/naming-convention + "DEVCHATPYTHON": UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3", + // eslint-disable-next-line @typescript-eslint/naming-convention + "PYTHONLIBPATH": UiUtilWrapper.extensionPath() + "/tools/site-packages", + // eslint-disable-next-line @typescript-eslint/naming-convention + "PYTHONPATH": UiUtilWrapper.extensionPath() + "/tools/site-packages", + // eslint-disable-next-line @typescript-eslint/naming-convention + "OPENAI_API_KEY": llmModelData.api_key, + // eslint-disable-next-line @typescript-eslint/naming-convention + ...llmModelData.api_base? { "OPENAI_API_BASE": llmModelData.api_base } : {} + }; + + // build process options + const spawnAsyncOptions = { + maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB + cwd: UiUtilWrapper.workspaceFoldersFirstPath(), + env: envs + }; + + // save llm model config + await saveModelSettings(); + + logger.channel()?.info(`api_key: ${llmModelData.api_key.replace(/^(.{4})(.*)(.{4})$/, (_, first, middle, last) => first + middle.replace(/./g, '*') + last)}`); + logger.channel()?.info(`api_base: ${llmModelData.api_base}`); + + // run command + // handle stdout as steam mode + let receviedStdout = ""; + const onStdoutPartial = (stdout: string) => { + receviedStdout += stdout; + const data = this.parseOutData(receviedStdout, true); + onData(data); + }; + // run command + const pythonApp = UiUtilWrapper.getConfiguration("DevChat", "PythonForChat") || "python3"; + logger.channel()?.info(`Running devchat:${pythonApp} ${args.join(" ")}`); + const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(pythonApp, args, spawnAsyncOptions, onStdoutPartial, undefined, undefined, undefined); + // handle result + assertValue(code !== 0, stderr || "Command exited with error code"); + const responseData = this.parseOutData(stdout, false); + await this.logInsert(options.context, content, responseData.response, options.parent); + const logs = await this.log({"maxCount": 1}); + assertValue(!logs || !logs.length, "Failed to insert devchat log"); + // return result + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + "prompt-hash": logs[0]['hash'], + user: "", + date: "", + response: stdout, + // eslint-disable-next-line @typescript-eslint/naming-convention + finish_reason: "", + isError: false, + }; + } catch (error: any) { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + "prompt-hash": "", + user: "", + date: "", + response: `Error: ${error.message}`, + // eslint-disable-next-line @typescript-eslint/naming-convention + finish_reason: "error", + isError: true, + }; + } + } + + async logInsert(contexts: string[] | undefined, request: string, response: string, parent: string | undefined): Promise { + try { + // build log data + const llmModelData = await ApiKeyManager.llmModel(); + const contextContentList = await this.loadContextsFromFiles(contexts); + const contextWithRoleList = contextContentList.map(content => { + return { + "role": "system", + "content": `${content}` + }; + }); + + let logData = { + "model": llmModelData?.model || "gpt-3.5-turbo", + "messages": [ + { + "role": "user", + "content": request + }, + { + "role": "assistant", + "content": response + }, + ...contextWithRoleList + ], + "timestamp": Math.floor(Date.now()/1000), + // eslint-disable-next-line @typescript-eslint/naming-convention + "request_tokens": 1, + // eslint-disable-next-line @typescript-eslint/naming-convention + "response_tokens": 1, + ...parent? {"parent": parent} : {} + }; + + // build args for log insert + const args = ["-m", "devchat", "log", "--insert", JSON.stringify(logData)]; + + const {code, stdout, stderr} = await this.runCommand(args); + + assertValue(code !== 0, stderr || `Command exited with ${code}`); + assertValue(stdout.indexOf('Failed to insert log') >= 0, stdout); + assertValue(stderr, stderr); + + return true; + } catch (error: any) { + logger.channel()?.error(`Failed to insert log: ${error.message}`); + logger.channel()?.show(); + return false; + } + } + + async delete(hash: string): Promise { + try { + // build args for log delete + const args = ["-m", "devchat", "log", "--delete", hash]; + + const {code, stdout, stderr} = await this.runCommand(args); + + assertValue(code !== 0, stderr || `Command exited with ${code}`); + assertValue(stdout.indexOf('Failed to delete prompt') >= 0, stdout); + assertValue(stderr, stderr); + + return true; + } catch (error: any) { + logger.channel()?.error(`Failed to delete log: ${error.message}`); + logger.channel()?.show(); + return false; + } + } + + async log(options: LogOptions = {}): Promise { + try { + const args = this.buildLogArgs(options); + + const {code, stdout, stderr} = await this.runCommand(args); + + assertValue(code !== 0, stderr || `Command exited with ${code}`); + assertValue(stderr, stderr); + + const logs = JSON.parse(stdout.trim()).reverse(); + for (const log of logs) { + log.response = log.responses[0]; + delete log.responses; + } + return logs; + } catch (error: any) { + logger.channel()?.error(`Failed to get logs: ${error.message}`); + logger.channel()?.show(); + return []; + } + } + + async commands(): Promise { + try { + const args = ["-m", "devchat", "run", "--list"]; + + const {code, stdout, stderr} = await this.runCommand(args); + + assertValue(code !== 0, stderr || `Command exited with ${code}`); + assertValue(stderr, stderr); + + const commands = JSON.parse(stdout.trim()); + + return commands; + } catch (error: any) { + logger.channel()?.error(`Error: ${error.message}`); + logger.channel()?.show(); + return []; + } + } + + async updateSysCommand(): Promise { + try { + const args = ["-m", "devchat", "run", "--update-sys"]; + + const {code, stdout, stderr} = await this.runCommand(args); + + assertValue(code !== 0, stderr || `Command exited with ${code}`); + assertValue(stderr, stderr); + + logger.channel()?.info(`${stdout}`); + return stdout; + } catch (error: any) { + logger.channel()?.error(`Error: ${error.message}`); + logger.channel()?.show(); + return ""; + } + } + + async topics(): Promise { + try { + const args = ["-m", "devchat", "topic", "-l"]; + + const {code, stdout, stderr} = await this.runCommand(args); + + assertValue(code !== 0, stderr || `Command exited with ${code}`); + assertValue(stderr, stderr); + + const topics = JSON.parse(stdout.trim()).reverse(); + for (const topic of topics) { + if (topic.root_prompt.responses) { + topic.root_prompt.response = topic.root_prompt.responses[0]; + delete topic.root_prompt.responses; + } + } + return topics; + } catch (error: any) { + logger.channel()?.error(`Error: ${error.message}`); + logger.channel()?.show(); + return []; } - return devChat; } } diff --git a/src/toolwrapper/dtm.ts b/src/toolwrapper/dtm.ts index 9a22cdb..30a5fc7 100644 --- a/src/toolwrapper/dtm.ts +++ b/src/toolwrapper/dtm.ts @@ -1,7 +1,3 @@ -import { spawn } from "child_process"; -import * as path from 'path'; -import * as fs from 'fs'; - import { logger } from "../util/logger"; import { CommandRun } from "../util/commonUtil"; import { UiUtilWrapper } from "../util/uiUtil"; @@ -27,7 +23,6 @@ class DtmWrapper { const result = await this.commandRun.spawnAsync("git", ['commit', '-m', commitMsg], { cwd: this.workspaceDir }, undefined, undefined, undefined, undefined); return { status: result.exitCode || 0, message: result.stdout, log: result.stderr }; } catch (error) { - // 处理 runCommand 中的 reject 错误 logger.channel()?.error(`Error: ${error}`); logger.channel()?.show(); return error as DtmResponse; @@ -40,7 +35,6 @@ class DtmWrapper { const result = await this.commandRun.spawnAsync("git", ['commit', '-am', commitMsg], { cwd: this.workspaceDir }, undefined, undefined, undefined, undefined); return { status: result.exitCode || 0, message: result.stdout, log: result.stderr }; } catch (error) { - // 处理 runCommand 中的 reject 错误 logger.channel()?.error(`Error: ${error}`); logger.channel()?.show(); return error as DtmResponse; diff --git a/src/topic/topicManager.ts b/src/topic/topicManager.ts index bc355c0..28fdcab 100644 --- a/src/topic/topicManager.ts +++ b/src/topic/topicManager.ts @@ -124,6 +124,12 @@ export class TopicManager { } } + deleteMessageOnCurrentTopic(messageHash: string): void { + if (this.currentTopicId) { + TopicManager.getInstance().deleteMessage(this.currentTopicId, messageHash); + } + } + getTopic(topicId: string): Topic | undefined { /** * 获取topic diff --git a/src/util/check.ts b/src/util/check.ts new file mode 100644 index 0000000..2bc8d81 --- /dev/null +++ b/src/util/check.ts @@ -0,0 +1,6 @@ + +export function assertValue(value: any, message: string) { + if (value) { + throw new Error(message); + } +} \ No newline at end of file diff --git a/src/util/extensionContext.ts b/src/util/extensionContext.ts index adc3b25..442d43f 100644 --- a/src/util/extensionContext.ts +++ b/src/util/extensionContext.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { DevChatViewProvider } from '../panel/devchatView'; -class ExtensionContextHolder { +export class ExtensionContextHolder { private static _context: vscode.ExtensionContext | undefined; private static _provider: DevChatViewProvider | undefined; @@ -21,5 +21,3 @@ class ExtensionContextHolder { return this._provider; } } - -export default ExtensionContextHolder; diff --git a/src/util/uiUtil_vscode.ts b/src/util/uiUtil_vscode.ts index 68ed0d2..086a7bf 100644 --- a/src/util/uiUtil_vscode.ts +++ b/src/util/uiUtil_vscode.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import ExtensionContextHolder from './extensionContext'; +import { ExtensionContextHolder } from './extensionContext'; import { UiUtil } from './uiUtil'; import { logger } from './logger'; diff --git a/test/action/actionManager.test.ts b/test/action/actionManager.test.ts deleted file mode 100644 index c906b91..0000000 --- a/test/action/actionManager.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { expect } from 'chai'; -import 'mocha'; -import ActionManager from '../../src/action/actionManager'; -import { Action } from '../../src/action/customAction'; - -describe('ActionManager', () => { - const testAction: Action = { - name: 'testAction', - description: 'Test action for unit testing', - type: ['test'], - action: 'test', - handler: [], - args: [], - handlerAction: async () => ({ exitCode: 0, stdout: '', stderr: '' }), - }; - - it('should register and retrieve actions', () => { - const actionManager = ActionManager.getInstance(); - actionManager.registerAction(testAction); - const actionList = actionManager.getActionList(); - expect(actionList).to.contain(testAction); - }); - - it('should return an error for action with empty args', async () => { - const actionManager = ActionManager.getInstance(); - const testActionWithEmptyArgs: Action = { - ...testAction, - name: 'testActionWithEmptyArgs', - args: [], - }; - actionManager.registerAction(testActionWithEmptyArgs); - const actionName = 'testActionWithEmptyArgs'; - const content = { - command: 'test', - content: 'test content', - fileName: 'test.txt', - }; - const result = await actionManager.applyAction(actionName, content); - expect(result.exitCode).to.equal(-1); - expect(result.stdout).to.equal(''); - expect(result.stderr).to.equal('Action testActionWithEmptyArgs has no args'); - }); - it('should apply action with valid args correctly', async () => { - const actionManager = ActionManager.getInstance(); - const testActionWithArgs: Action = { - ...testAction, - name: 'testActionWithArgs', - args: [ - { - name: 'arg1', - description: 'arg1 description', - type: 'string', - from: 'content.fileName', - }, - ], - }; - actionManager.registerAction(testActionWithArgs); - const actionName = 'testActionWithArgs'; - const content = { - command: 'test', - content: 'test content', - fileName: 'test.txt', - }; - const result = await actionManager.applyAction(actionName, content); - expect(result.exitCode).to.equal(0); - expect(result.stdout).to.equal(''); - expect(result.stderr).to.equal(''); - }); - - it('should apply action with content.content.xxx type args correctly', async () => { - const actionManager = ActionManager.getInstance(); - const testActionWithNestedArgs: Action = { - ...testAction, - name: 'testActionWithNestedArgs', - args: [ - { - name: 'arg1', - description: 'arg1 description', - type: 'string', - from: 'content.content.field1', - }, - ], - handlerAction: async (args) => { - if (args.arg1 === 'value1') { - return { exitCode: 0, stdout: '', stderr: '' }; - } else { - return { exitCode: -1, stdout: '', stderr: 'Incorrect arg1 value' }; - } - }, - }; - actionManager.registerAction(testActionWithNestedArgs); - const actionName = 'testActionWithNestedArgs'; - const content = { - command: 'test', - content: JSON.stringify({ field1: 'value1' }), - fileName: 'test.txt', - }; - const result = await actionManager.applyAction(actionName, content); - expect(result.exitCode).to.equal(0); - expect(result.stdout).to.equal(''); - expect(result.stderr).to.equal(''); - }); - - it('should return error when content is missing required args', async () => { - const actionManager = ActionManager.getInstance(); - const testActionWithMissingArgs: Action = { - ...testAction, - name: 'testActionWithMissingArgs', - args: [ - { - name: 'arg1', - description: 'arg1 description', - type: 'string', - from: 'content.field1', - }, - ], - handlerAction: async (args) => { - return { exitCode: 0, stdout: '', stderr: '' }; - }, - }; - actionManager.registerAction(testActionWithMissingArgs); - const actionName = 'testActionWithMissingArgs'; - const content = { - command: 'test', - content: JSON.stringify({}), - fileName: 'test.txt', - }; - const result = await actionManager.applyAction(actionName, content); - expect(result.exitCode).to.equal(-1); - expect(result.stdout).to.equal(''); - expect(result.stderr).to.equal('Action testActionWithMissingArgs arg arg1 from content.field1 is undefined'); - }); - - it('should trigger handlerAction of CommandRunAction', async () => { - class TestAction implements Action { - name: string; - description: string; - type: string[]; - action: string; - handler: string[]; - args: { name: string; description: string; type: string; as?: string | undefined; from: string; }[]; - - constructor() { - this.name = 'testAction2'; - this.description = 'Test action for unit testing'; - this.type = ['test']; - this.action = 'test'; - this.handler = []; - this.args = []; - } - async handlerAction(content: any): Promise<{ exitCode: number; stdout: string; stderr: string }> { - return { - exitCode: 0, - stdout: 'Test action executed successfully', - stderr: '', - }; - } - } - - const actionManager = ActionManager.getInstance(); - const testAction = new TestAction(); - actionManager.registerAction(testAction); - - const commandRunActionContent = { - command: 'testAction2', - args: {}, - }; - - const content = { - command: 'command_run', - content: JSON.stringify(commandRunActionContent), - fileName: 'test.txt', - }; - - const result = await actionManager.applyAction('command_run', content); - expect(result.exitCode).to.equal(0); - expect(result.stdout).to.equal('Test action executed successfully'); - expect(result.stderr).to.equal(''); - }); -}); \ No newline at end of file diff --git a/test/command/commandManager.test.ts b/test/command/commandManager.test.ts deleted file mode 100644 index da2826f..0000000 --- a/test/command/commandManager.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; -import CommandManager, { Command } from '../../src/command/commandManager'; -import CustomCommands, { Command as CCommand } from '../../src/command/customCommand'; - -describe('CommandManager', () => { - let commandManager: CommandManager; - - beforeEach(() => { - commandManager = CommandManager.getInstance(); - }); - - afterEach(() => { - // Reset the command list after each test - commandManager['commands'] = []; - }); - - it('should register a command', () => { - const command: Command = { - name: 'test', - pattern: 'test', - description: 'Test command', - args: 0, - handler: async (commandName: string, userInput: string) => { - return 'Test result'; - }, - }; - - commandManager.registerCommand(command); - expect(commandManager['commands']).to.include(command); - }); - - it('should return the command list', () => { - const command: Command = { - name: 'test', - pattern: 'test', - description: 'Test command', - args: 0, - handler: async (commandName: string, userInput: string) => { - return 'Test result'; - }, - }; - - commandManager.registerCommand(command); - expect(commandManager.getCommandList()).to.include(command); - }); - - it('should process text with a command', async () => { - const command: Command = { - name: 'test', - pattern: 'test', - description: 'Test command', - args: 0, - handler: async (commandName: string, userInput: string) => { - return 'Test result'; - }, - }; - - commandManager.registerCommand(command); - const result = await commandManager.processText('/test'); - expect(result).to.equal('Test result'); - }); - - it('should process text with a custom command', async () => { - const customCommand: CCommand = { - name: 'customTest', - pattern: 'customTest', - description: 'Custom test command', - message: 'Custom test result', - args: 0, - show: true, - default: false, - instructions: [] - }; - - CustomCommands.getInstance().regCommand(customCommand); - const result = await commandManager.processText('/customTest'); - expect(result).to.equal(' Custom test result'); - }); - - it('should match /xxx with space or newline, but not with other characters', async () => { - const command: Command = { - name: 'xxx', - pattern: 'xxx', - description: 'Test command', - args: 0, - handler: async (commandName: string, userInput: string) => { - return 'Matched'; - }, - }; - - commandManager.registerCommand(command); - - const result1 = await commandManager.processText('/xxx someother text'); - expect(result1).to.equal('Matched someother text'); - - const result2 = await commandManager.processText('/xxx\n'); - expect(result2).to.equal('Matched\n'); - - const result3 = await commandManager.processText('/xxx-123'); - expect(result3).to.equal('/xxx-123'); - - const result4 = await commandManager.processText('/xxx123'); - expect(result4).to.equal('/xxx123'); - }); - - it('should process text with a command containing two arguments', async () => { - const command: Command = { - name: 'test', - pattern: 'xxx {{prompt}}', - description: 'Test command with two arguments ${1} and ${2}', - args: 2, - handler: async (commandName: string, userInput: string) => { - return `Test result with argument: ["arg1", "arg2"]`; - }, - }; - - commandManager.registerCommand(command); - const result = await commandManager.processText('/xxx {{["arg1", "arg2"]}}'); - expect(result).to.equal('Test result with argument: ["arg1", "arg2"]'); -}); -}); \ No newline at end of file diff --git a/test/command/customCommand.test.ts b/test/command/customCommand.test.ts deleted file mode 100644 index e411c79..0000000 --- a/test/command/customCommand.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; -import mockFs from 'mock-fs'; -import * as fs from 'fs'; -import * as path from 'path'; -import CustomCommands, { Command } from '../../src/command/customCommand'; - - -describe('CustomCommands', () => { - let customCommands: CustomCommands; - - beforeEach(() => { - customCommands = CustomCommands.getInstance(); - }); - - afterEach(() => { - // Reset the command list after each test - customCommands['commands'] = []; - mockFs.restore(); - }); - - it('should parse commands from workflows directory', () => { - // Mock the file system with two directories, one with _setting_.json and one without - mockFs({ - 'workflows': { - "some": { - "command": { - 'command1': { - '_setting_.json': JSON.stringify({ - pattern: 'command1', - description: 'Command 1', - message: 'Command 1 message', - default: false, - show: true, - instructions: ['instruction1', 'instruction2'], - }), - }, - 'command2': { - // No _setting_.json file - }, - } - } - } - }); - - const workflowsDir = path.join(process.cwd(), 'workflows'); - customCommands.parseCommands(workflowsDir); - - const expectedResult: Command[] = [ - { - name: 'command1', - pattern: 'command1', - description: 'Command 1', - message: 'Command 1 message', - default: false, - args: 0, - show: true, - instructions: ['instruction1', 'instruction2'], - }, - ]; - - expectedResult[0].instructions = [path.join(workflowsDir, 'some', 'command', 'command1', 'instruction1'), path.join(workflowsDir, 'some', 'command', 'command1', 'instruction2')]; - expect(customCommands['commands']).to.deep.equal(expectedResult); - }); - - it('should register a custom command', () => { - const command: Command = { - name: 'test', - pattern: 'test', - description: 'Test command', - message: 'Test message', - default: false, - args: 0, - show: true, - instructions: ['instruction1', 'instruction2'], - }; - - customCommands.regCommand(command); - expect(customCommands['commands']).to.include(command); - }); - - it('should get a custom command by name', () => { - const command: Command = { - name: 'test', - pattern: 'test', - description: 'Test command', - message: 'Test message', - default: false, - args: 0, - show: true, - instructions: ['instruction1', 'instruction2'], - }; - - customCommands.regCommand(command); - const foundCommand = customCommands.getCommand('test'); - expect(foundCommand).to.deep.equal(command); - }); - - it('should handle a custom command', () => { - const command: Command = { - name: 'test', - pattern: 'test', - description: 'Test command', - message: 'Test message', - default: false, - args: 0, - show: true, - instructions: ['instruction1', 'instruction2'], - }; - - customCommands.regCommand(command); - const result = customCommands.handleCommand('test', ''); - expect(result).to.equal('[instruction|instruction1] [instruction|instruction2] Test message'); - }); - - it('should handle a custom command with args', () => { - const command: Command = { - name: 'test', - pattern: 'test {{prompt}}', - description: 'Test command', - message: 'Test message "$1","$2"', - default: false, - args: 0, - show: true, - instructions: ['instruction1', 'instruction2'], - }; - - customCommands.regCommand(command); - const result = customCommands.handleCommand('test', '["v1", "v2"]'); - expect(result).to.equal('[instruction|instruction1] [instruction|instruction2] Test message "v1","v2"'); - }); -}); \ No newline at end of file diff --git a/test/context/customContext.test.ts b/test/context/customContext.test.ts index f16ca48..6826f7f 100644 --- a/test/context/customContext.test.ts +++ b/test/context/customContext.test.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import CustomContexts from '../../src/context/customContext'; import fs from 'fs'; import path from 'path'; -import CustomCommands, { Command } from '../../src/command/customCommand'; describe('CustomContexts', () => { const workflowsDir = path.join(__dirname, 'test-workflows'); diff --git a/test/context/loadContexts.test.ts b/test/context/loadContexts.test.ts index 223a97d..b93b874 100644 --- a/test/context/loadContexts.test.ts +++ b/test/context/loadContexts.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import '../../src/context/loadContexts'; -import ChatContextManager from '../../src/context/contextManager'; +import { ChatContextManager } from '../../src/context/contextManager'; import { gitDiffCachedContext } from '../../src/context/contextGitDiffCached'; import { gitDiffContext } from '../../src/context/contextGitDiff'; import { customCommandContext } from '../../src/context/contextCustomCommand'; diff --git a/test/handler/sendMessageBase.test.ts b/test/handler/sendMessageBase.test.ts index 736e15e..171dc7c 100644 --- a/test/handler/sendMessageBase.test.ts +++ b/test/handler/sendMessageBase.test.ts @@ -1,14 +1,9 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { Context } from 'mocha'; import sinon from 'sinon'; import * as path from 'path'; -import { parseMessage, getInstructionFiles, parseMessageAndSetOptions, handleTopic, handlerResponseText, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase'; -import DevChat, { ChatResponse } from '../../src/toolwrapper/devchat'; -import CommandManager from '../../src/command/commandManager'; -import messageHistory from '../../src/util/messageHistory'; -import { TopicManager } from '../../src/topic/topicManager'; -import CustomCommands from '../../src/command/customCommand'; +import { parseMessage, parseMessageAndSetOptions, TopicUpdateHandler, processChatResponse, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase'; +import { ChatResponse } from '../../src/toolwrapper/devchat'; import { UiUtilWrapper } from '../../src/util/uiUtil'; import * as dotenv from 'dotenv'; @@ -42,13 +37,6 @@ describe('sendMessageBase', () => { }); }); - describe('getInstructionFiles', () => { - it('should return instruction files', () => { - const result = getInstructionFiles(); - expect(result).to.be.an('array'); - }); - }); - describe('parseMessageAndSetOptions', () => { it('should parse message and set options correctly', async () => { const message = { @@ -69,7 +57,7 @@ describe('sendMessageBase', () => { }); - describe('handleTopic', () => { + describe('TopicUpdateHandler.processTopicChangeAfterChat', () => { it('should handle topic correctly', async () => { const parentHash = 'somehash'; const message = { @@ -84,12 +72,12 @@ describe('sendMessageBase', () => { 'prompt-hash': 'responsehash' }; - await handleTopic(parentHash, message, chatResponse); + await TopicUpdateHandler.processTopicChangeAfterChat(parentHash, message, chatResponse); // Check if the topic was updated correctly }); }); - describe('handlerResponseText', () => { + describe('processChatResponse', () => { it('should handle response text correctly when isError is false', async () => { const partialDataText = 'Partial data'; const chatResponse: ChatResponse = { @@ -101,7 +89,7 @@ describe('sendMessageBase', () => { 'prompt-hash': 'responsehash' }; - const result = await handlerResponseText(partialDataText, chatResponse); + const result = await processChatResponse(chatResponse); expect(result).to.equal('Hello, user!'); }); @@ -116,7 +104,7 @@ describe('sendMessageBase', () => { 'prompt-hash': 'responsehash' }; - const result = await handlerResponseText(partialDataText, chatResponse); + const result = await processChatResponse(chatResponse); expect(result).to.equal('Error occurred!'); }); }); diff --git a/tools b/tools index d1f8662..314bb32 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit d1f8662061e9b857ac78db362077c1d76868377e +Subproject commit 314bb32c4790bc3eb8b1044ffc639ff3c85d562c