remove command and action extension

This commit is contained in:
bobo.yang 2023-11-29 23:34:15 +08:00
parent 1e09a858a5
commit dc1836510d
62 changed files with 1270 additions and 3522 deletions

473
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<CommandResult> {
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<CommandResult> {
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<CommandResult> {
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();
}
}
}

View File

@ -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<CommandResult> {
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}`};
}
}
};

View File

@ -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<CommandResult> {
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}`};
}
}
};

View File

@ -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<CommandResult>;
}
// 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;
}
}

View File

@ -1,56 +0,0 @@
/*
vscode commandRunVSCommandAction
*/
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<CommandResult> {
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}`};
}
}
}

View File

@ -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<CommandResult> {
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<CommandResult> {
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.DocumentSymbol[]>(
'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}`};
}
}
};

View File

@ -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<string[]> {
const symbolPosition = await getSymbolPosition(symbolName, symbolline, symbolFile);
if (!symbolPosition) {
return [];
}
// get definition of symbol
const refLocations = await vscode.commands.executeCommand<vscode.Location[] | vscode.LocationLink[]>(
'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.DocumentSymbol[]>(
'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<CommandResult> {
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}`};
}
}
};

View File

@ -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<CommandResult> {
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.DocumentSymbol[]>(
'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}`};
}
}
};

View File

@ -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<any[]>(
'vscode.executeDefinitionProvider',
vscode.Uri.file(filename),
position
);
if (!defLocations || defLocations.length === 0) {
defLocations = await vscode.commands.executeCommand<any[]>(
'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<vscode.Position | undefined> {
// 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<string[]> {
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.Location[]>(
'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<string> = 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.DocumentSymbol[]>(
'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<CommandResult> {
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}`};
}
}
};

View File

@ -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<string>;
}
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<Command[]> {
// 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<string> {
// 定义一个异步函数来处理单个命令
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(
`\\/(?<command>${escapedPattern.replace('{{prompt}}', '(?<userInput>.+?)')})(?=\\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<string> {
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;

View File

@ -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;

View File

@ -1,6 +0,0 @@
import CommandManager from './commandManager';
const commandManager = CommandManager.getInstance();
// 注册命令

View File

@ -12,7 +12,7 @@ export interface ChatContext {
handler: () => Promise<string[]>;
}
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<string[]> {
async handleContextSelected(command: string): Promise<string[]> {
for (const contextObj of this.contexts) {
if (contextObj.name === command) {
return await contextObj.handler();
@ -77,4 +77,3 @@ export interface ChatContext {
}
}
export default ChatContextManager;

View File

@ -1,4 +1,4 @@
import ChatContextManager from './contextManager';
import { ChatContextManager } from './contextManager';
import { gitDiffCachedContext } from './contextGitDiffCached';
import { gitDiffContext } from './contextGitDiff';
import { customCommandContext } from './contextCustomCommand';

View File

@ -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 ...`);

View File

@ -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: ''});

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}

View File

@ -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<void> {
const contextStrList = await ChatContextManager.getInstance().processText(message.selected);
for (const contextStr of contextStrList) {
MessageHandler.sendMessage(panel, { command: 'appendContext', context: contextStr });
}
}

View File

@ -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<void> {
const contextStr = await handleRefCommand(message.refCommand);
MessageHandler.sendMessage(panel, { command: 'appendContext', context: contextStr });
return;
}

View File

@ -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<void> {
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();
}
}

View File

@ -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<void> {
await applyCode(message.content);
return;
}

View File

@ -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<void> {
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<void> {
await applyCode(message.content);
}
regInMessage({command: 'code_file_apply', content: '', fileName: ''});
export async function replaceCodeBlockToFile(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
await applyCodeFile(message.content, message.fileName);
}

View File

@ -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<void> {
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<void> {
await applyCodeFile(message.content, message.fileName);
return;
}

View File

@ -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<void> {
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}`);
}
}

View File

@ -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<void> {
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<void> {
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}`);
}
}

View File

@ -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<void> {
const newText = await CommandManager.getInstance().processText(message.text);
MessageHandler.sendMessage(panel, { command: 'convertCommand', result: newText });
return;
}

View File

@ -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<string | undefined> {
export async function getFileContent(fileName: string): Promise<string | undefined> {
const readFile = util.promisify(fs.readFile);
try {
// Read file content from fileName
@ -122,7 +122,7 @@ async function getNewCode(message: any): Promise<string | undefined> {
}
regInMessage({ command: 'show_diff', content: '', fileName: '' });
export async function showDiff(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
export async function applyCodeWithDiff(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
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<void> {
const newCode = await getNewCode(message);
if (!newCode) {
return;
}
diffView(newCode, message.fileName);
return;
}

View File

@ -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<void> {
export async function getFeatureToggles(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
const featureTaggles = FTs();
MessageHandler.sendMessage(panel, {command: 'featureToggles', features: featureTaggles});
}

View File

@ -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: <natural language description> }
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: <history messages> }
// <history messages> 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: <command list> }
messageHandler.registerHandler('regCommandList', regCommandListByDevChatRun);
messageHandler.registerHandler('regCommandList', getWorkflowCommandList);
// Register the context list
// Response: { command: 'regContextList', result: <context list> }
messageHandler.registerHandler('regContextList', regContextList);
messageHandler.registerHandler('regContextList', getWorkflowContextList);
// Send a message, send the message entered by the user to AI
// Response:
// { command: 'receiveMessagePartial', text: <response message text>, user: <user>, date: <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: <context file> }
messageHandler.registerHandler('addRefCommandContext', addRefCommandContext);
messageHandler.registerHandler('show_diff', applyCodeWithDiff);
// Get context details
// Response: { command: 'contextDetailResponse', 'file':<context file>, result: <context file content> }
// <context file content> 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: <action list> }
messageHandler.registerHandler('regActionList', regActionList);
// Apply action for code block
// Response: none
messageHandler.registerHandler('applyAction', applyAction);
// Delete chat message
// Response: { command: 'deletedChatMessage', result: <message id> }
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);

View File

@ -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<void> {
export async function getHistoryMessages(message: {command: string, page: number}, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
// if history message has load, send it to webview
const maxCount = Number(UiUtilWrapper.getConfiguration('DevChat', 'maxLogCount'));
const skip = maxCount * (message.page ? message.page : 0);

View File

@ -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<void> {
export async function getValidLlmModelList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
const modelList = await ApiKeyManager.getValidModels();
MessageHandler.sendMessage(panel, { command: 'regModelList', result: modelList });

View File

@ -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<void> } = {};
@ -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);
}
}
}
}
}
}

View File

@ -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<void> {
const actionList = ActionManager.getInstance().getActionList();
MessageHandler.sendMessage(panel, { command: 'regActionList', result: actionList });
}

View File

@ -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,66 +32,9 @@ 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<void> {
commandRunner?.input(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<void> {
// 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
function writeContextInfoToTempFiles(message : any) : string[] {
let tempFiles: string[] = [];
message.old_text = message.old_text || message.text;
// Handle the contextInfo field in the message
if (Array.isArray(message.contextInfo)) {
@ -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<void> {
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<void> {
// 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<void> {
// 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<void> {
stopDevChatBase(message);
if (commandRunner) {
commandRunner.stop();
commandRunner = null;
}
}
regInMessage({command: 'deleteChatMessage', hash: 'xxx'});

View File

@ -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<void>} - A Promise that resolves when the topic change has been processed.
*/
public static async processTopicChangeAfterChat(parentHash:string | undefined, message: any, chatResponse: ChatResponse): Promise<void> {
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 [parsedMessage, chatOptions];
}
return instructionFiles;
/**
* 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<void> {
messageHistory.add({
request: message.text,
text: chatResponse.response,
parentHash,
hash: chatResponse['prompt-hash'],
user: chatResponse.user,
date: chatResponse.date
});
}
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;
}
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<string|undefined> {
/**
* 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}`);
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");
// 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
};
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) {
} catch (error: any) {
logger.channel()?.error(`Error occurred while sending response: ${error.message}`);
return ;
} finally {
UserStopHandler.resumeUserInteraction();
}
}
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<void>} - A Promise that resolves when the DevChat and user interaction have been stopped.
*/
export async function stopDevChatBase(message: any): Promise<void> {
logger.channel()?.info(`Stopping devchat`);
userStop = true;
UserStopHandler.stopUserInteraction();
devChat.stop();
}
export async function insertDevChatLog(message: any, request: string, response: string): Promise<string | undefined> {
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;
}
}
// delete a chat message
// each message is identified by hash
/**
* 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<boolean>} - 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<boolean> {
// if hash is undefined, return
if (!message.hash) {
return true;
}
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);
if (bSuccess) {
let topicId = TopicManager.getInstance().currentTopicId;
if (topicId) {
TopicManager.getInstance().deleteMessage(topicId, 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;
}
}
return bSuccess;
/**
* Sends a text message to the DevChat.
*
* @param {string} text - The text message to be sent.
* @returns {Promise<void>} - A Promise that resolves when the message has been sent.
*/
export async function sendTextToDevChat(text: string): Promise<void> {
return devChat.input(text);
}

View File

@ -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<void> {
export async function doVscodeCommand(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
// execute vscode command
// message.content[0]: vscode command
// message.content[1:]: args for command

View File

@ -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<void> {
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) + '}}');
export interface Command {
name: string;
pattern: string;
description: string;
args: number;
handler: (commandName: string, userInput: string) => Promise<string>;
}
return command;
async function getCommandListByDevChatRun(includeHide: boolean = false): Promise<Command[]> {
// 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<void> {
export async function getWorkflowCommandList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
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 });
}

View File

@ -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<void> {
export async function getWorkflowContextList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
const contextList = ChatContextManager.getInstance().getContextList();
MessageHandler.sendMessage(panel, { command: 'regContextList', result: contextList });
return;
}

View File

@ -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<string | undefined> {
/*
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:
<<Start>>
{"content": "data"}
<<End>>
*/
const outputWitchCache = this._cacheOut + outputStr;
this._cacheOut = "";
let outputResult = "";
let curPos = 0;
while (true) {
const startPos = outputWitchCache.indexOf('<<Start>>', 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('<<End>>', 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('<<Start>>');
const endPos = outputStr.lastIndexOf('<<End>>');
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<<Start>>\n${data}\n<<End>>\n`;
this._commandRunner?.write(userInputWithFlag);
}
public async run(workflow: string, commandDefines: any, message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
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 });
}
}
}
}

View File

@ -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}/`;
});
}

View File

@ -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) {

View File

@ -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();

View File

@ -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 {

View File

@ -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<string[]> {
if (!contexts) {
return [];
}
async buildArgs(options: ChatOptions): Promise<string[]> {
let args = ["prompt"];
const loadedContexts: string[] = [];
for (const context of contexts) {
const contextContent = await getFileContent(context);
if (!contextContent) {
continue;
}
loadedContexts.push(contextContent);
}
return loadedContexts;
}
private async buildArgs(options: ChatOptions): Promise<string[]> {
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) {
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<ChatResponse> {
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<boolean> {
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<LogEntry[]> {
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<CommandEntry[]> {
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<string> {
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<string> {
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<TopicEntry[]> {
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<ChatResponse> {
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<boolean> {
try {
// build log data
const llmModelData = await ApiKeyManager.llmModel();
const contextContentList = await this.loadContextsFromFiles(contexts);
const contextWithRoleList = contextContentList.map(content => {
return {
"role": "system",
"content": `<context>${content}</context>`
};
});
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<boolean> {
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<LogEntry[]> {
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<CommandEntry[]> {
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<string> {
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<TopicEntry[]> {
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;
}
}

View File

@ -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;

View File

@ -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

6
src/util/check.ts Normal file
View File

@ -0,0 +1,6 @@
export function assertValue(value: any, message: string) {
if (value) {
throw new Error(message);
}
}

View File

@ -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;

View File

@ -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';

View File

@ -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('');
});
});

View File

@ -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"]');
});
});

View File

@ -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"');
});
});

View File

@ -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');

View File

@ -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';

View File

@ -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!');
});
});

2
tools

@ -1 +1 @@
Subproject commit d1f8662061e9b857ac78db362077c1d76868377e
Subproject commit 314bb32c4790bc3eb8b1044ffc639ff3c85d562c