mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add new vscode shell integration to run commands right in terminal
This commit is contained in:
193
package-lock.json
generated
193
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-dev",
|
"name": "claude-dev",
|
||||||
"version": "1.5.33",
|
"version": "1.5.34",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-dev",
|
"name": "claude-dev",
|
||||||
"version": "1.5.33",
|
"version": "1.5.34",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
"default-shell": "^2.2.0",
|
"default-shell": "^2.2.0",
|
||||||
"delay": "^6.0.0",
|
"delay": "^6.0.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
"execa": "^9.3.0",
|
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"mammoth": "^1.8.0",
|
"mammoth": "^1.8.0",
|
||||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||||
@@ -29,7 +28,6 @@
|
|||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"web-tree-sitter": "^0.22.6"
|
"web-tree-sitter": "^0.22.6"
|
||||||
},
|
},
|
||||||
@@ -37,7 +35,7 @@
|
|||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "20.x",
|
"@types/node": "20.x",
|
||||||
"@types/vscode": "^1.82.0",
|
"@types/vscode": "^1.93.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||||
"@typescript-eslint/parser": "^7.11.0",
|
"@typescript-eslint/parser": "^7.11.0",
|
||||||
"@vscode/test-cli": "^0.0.9",
|
"@vscode/test-cli": "^0.0.9",
|
||||||
@@ -2829,24 +2827,6 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sec-ant/readable-stream": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@sindresorhus/merge-streams": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@smithy/abort-controller": {
|
"node_modules/@smithy/abort-controller": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz",
|
||||||
@@ -4547,9 +4527,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/vscode": {
|
"node_modules/@types/vscode": {
|
||||||
"version": "1.91.0",
|
"version": "1.93.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.91.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz",
|
||||||
"integrity": "sha512-PgPr+bUODjG3y+ozWUCyzttqR9EHny9sPAfJagddQjDwdtf66y2sDKJMnFZRuzBA2YtBGASqJGPil8VDUPvO6A==",
|
"integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -6144,44 +6124,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/execa": {
|
|
||||||
"version": "9.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.3.0.tgz",
|
|
||||||
"integrity": "sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sindresorhus/merge-streams": "^4.0.0",
|
|
||||||
"cross-spawn": "^7.0.3",
|
|
||||||
"figures": "^6.1.0",
|
|
||||||
"get-stream": "^9.0.0",
|
|
||||||
"human-signals": "^7.0.0",
|
|
||||||
"is-plain-obj": "^4.1.0",
|
|
||||||
"is-stream": "^4.0.1",
|
|
||||||
"npm-run-path": "^5.2.0",
|
|
||||||
"pretty-ms": "^9.0.0",
|
|
||||||
"signal-exit": "^4.1.0",
|
|
||||||
"strip-final-newline": "^4.0.0",
|
|
||||||
"yoctocolors": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.19.0 || >=20.5.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/execa/node_modules/is-plain-obj": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/extend": {
|
"node_modules/extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
@@ -6254,33 +6196,6 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/figures": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-unicode-supported": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/figures/node_modules/is-unicode-supported": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -6586,22 +6501,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-stream": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@sec-ant/readable-stream": "^0.4.1",
|
|
||||||
"is-stream": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-symbol-description": {
|
"node_modules/get-symbol-description": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
|
||||||
@@ -6912,15 +6811,6 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/human-signals": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.18.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/humanize-ms": {
|
"node_modules/humanize-ms": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||||
@@ -7300,18 +7190,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-stream": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-string": {
|
"node_modules/is-string": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
|
||||||
@@ -8715,18 +8593,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parse-ms": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -8867,21 +8733,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pretty-ms": {
|
|
||||||
"version": "9.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.0.0.tgz",
|
|
||||||
"integrity": "sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"parse-ms": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
@@ -9635,18 +9486,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-final-newline": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@@ -9787,14 +9626,6 @@
|
|||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tree-kill": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
|
||||||
"bin": {
|
|
||||||
"tree-kill": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tree-sitter-wasms": {
|
"node_modules/tree-sitter-wasms": {
|
||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.11.tgz",
|
||||||
@@ -10443,18 +10274,6 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/yoctocolors": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "20.x",
|
"@types/node": "20.x",
|
||||||
"@types/vscode": "^1.82.0",
|
"@types/vscode": "^1.93.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||||
"@typescript-eslint/parser": "^7.11.0",
|
"@typescript-eslint/parser": "^7.11.0",
|
||||||
"@vscode/test-cli": "^0.0.9",
|
"@vscode/test-cli": "^0.0.9",
|
||||||
@@ -144,7 +144,6 @@
|
|||||||
"default-shell": "^2.2.0",
|
"default-shell": "^2.2.0",
|
||||||
"delay": "^6.0.0",
|
"delay": "^6.0.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
"execa": "^9.3.0",
|
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"mammoth": "^1.8.0",
|
"mammoth": "^1.8.0",
|
||||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||||
@@ -153,7 +152,6 @@
|
|||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"web-tree-sitter": "^0.22.6"
|
"web-tree-sitter": "^0.22.6"
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/ClaudeDev.ts
110
src/ClaudeDev.ts
@@ -2,14 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
|||||||
import defaultShell from "default-shell"
|
import defaultShell from "default-shell"
|
||||||
import delay from "delay"
|
import delay from "delay"
|
||||||
import * as diff from "diff"
|
import * as diff from "diff"
|
||||||
import { execa, ExecaError, ResultPromise } from "execa"
|
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import osName from "os-name"
|
import osName from "os-name"
|
||||||
import pWaitFor from "p-wait-for"
|
import pWaitFor from "p-wait-for"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { serializeError } from "serialize-error"
|
import { serializeError } from "serialize-error"
|
||||||
import treeKill from "tree-kill"
|
|
||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { ApiHandler, buildApiHandler } from "./api"
|
import { ApiHandler, buildApiHandler } from "./api"
|
||||||
import { LIST_FILES_LIMIT, listFiles, parseSourceCodeForDefinitionsTopLevel } from "./parse-source-code"
|
import { LIST_FILES_LIMIT, listFiles, parseSourceCodeForDefinitionsTopLevel } from "./parse-source-code"
|
||||||
@@ -17,7 +15,7 @@ import { ClaudeDevProvider } from "./providers/ClaudeDevProvider"
|
|||||||
import { ApiConfiguration } from "./shared/api"
|
import { ApiConfiguration } from "./shared/api"
|
||||||
import { ClaudeRequestResult } from "./shared/ClaudeRequestResult"
|
import { ClaudeRequestResult } from "./shared/ClaudeRequestResult"
|
||||||
import { combineApiRequests } from "./shared/combineApiRequests"
|
import { combineApiRequests } from "./shared/combineApiRequests"
|
||||||
import { combineCommandSequences, COMMAND_STDIN_STRING } from "./shared/combineCommandSequences"
|
import { combineCommandSequences } from "./shared/combineCommandSequences"
|
||||||
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "./shared/ExtensionMessage"
|
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "./shared/ExtensionMessage"
|
||||||
import { getApiMetrics } from "./shared/getApiMetrics"
|
import { getApiMetrics } from "./shared/getApiMetrics"
|
||||||
import { HistoryItem } from "./shared/HistoryItem"
|
import { HistoryItem } from "./shared/HistoryItem"
|
||||||
@@ -28,6 +26,7 @@ import { truncateHalfConversation } from "./utils/context-management"
|
|||||||
import { regexSearchFiles } from "./utils/ripgrep"
|
import { regexSearchFiles } from "./utils/ripgrep"
|
||||||
import { extractTextFromFile } from "./utils/extract-text"
|
import { extractTextFromFile } from "./utils/extract-text"
|
||||||
import { getPythonEnvPath } from "./utils/get-python-env"
|
import { getPythonEnvPath } from "./utils/get-python-env"
|
||||||
|
import { TerminalManager } from "./integrations/TerminalManager"
|
||||||
|
|
||||||
const SYSTEM_PROMPT =
|
const SYSTEM_PROMPT =
|
||||||
async () => `You are Claude Dev, a highly skilled software developer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
async () => `You are Claude Dev, a highly skilled software developer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||||
@@ -257,6 +256,7 @@ type UserContent = Array<
|
|||||||
export class ClaudeDev {
|
export class ClaudeDev {
|
||||||
readonly taskId: string
|
readonly taskId: string
|
||||||
private api: ApiHandler
|
private api: ApiHandler
|
||||||
|
private terminalManager: TerminalManager
|
||||||
private customInstructions?: string
|
private customInstructions?: string
|
||||||
private alwaysAllowReadOnly: boolean
|
private alwaysAllowReadOnly: boolean
|
||||||
apiConversationHistory: Anthropic.MessageParam[] = []
|
apiConversationHistory: Anthropic.MessageParam[] = []
|
||||||
@@ -265,7 +265,6 @@ export class ClaudeDev {
|
|||||||
private askResponseText?: string
|
private askResponseText?: string
|
||||||
private askResponseImages?: string[]
|
private askResponseImages?: string[]
|
||||||
private lastMessageTs?: number
|
private lastMessageTs?: number
|
||||||
private executeCommandRunningProcess?: ResultPromise
|
|
||||||
private consecutiveMistakeCount: number = 0
|
private consecutiveMistakeCount: number = 0
|
||||||
private shouldSkipNextApiReqStartedMessage = false
|
private shouldSkipNextApiReqStartedMessage = false
|
||||||
private providerRef: WeakRef<ClaudeDevProvider>
|
private providerRef: WeakRef<ClaudeDevProvider>
|
||||||
@@ -282,6 +281,7 @@ export class ClaudeDev {
|
|||||||
) {
|
) {
|
||||||
this.providerRef = new WeakRef(provider)
|
this.providerRef = new WeakRef(provider)
|
||||||
this.api = buildApiHandler(apiConfiguration)
|
this.api = buildApiHandler(apiConfiguration)
|
||||||
|
this.terminalManager = new TerminalManager(provider.context)
|
||||||
this.customInstructions = customInstructions
|
this.customInstructions = customInstructions
|
||||||
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
||||||
|
|
||||||
@@ -731,10 +731,7 @@ export class ClaudeDev {
|
|||||||
|
|
||||||
abortTask() {
|
abortTask() {
|
||||||
this.abort = true // will stop any autonomously running promises
|
this.abort = true // will stop any autonomously running promises
|
||||||
const runningProcessId = this.executeCommandRunningProcess?.pid
|
this.terminalManager.disposeAll()
|
||||||
if (runningProcessId) {
|
|
||||||
treeKill(runningProcessId, "SIGTERM")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeTool(toolName: ToolName, toolInput: any): Promise<ToolResponse> {
|
async executeTool(toolName: ToolName, toolInput: any): Promise<ToolResponse> {
|
||||||
@@ -1420,92 +1417,41 @@ export class ClaudeDev {
|
|||||||
return "The user denied this operation."
|
return "The user denied this operation."
|
||||||
}
|
}
|
||||||
|
|
||||||
let userFeedback: { text?: string; images?: string[] } | undefined
|
try {
|
||||||
const sendCommandOutput = async (subprocess: ResultPromise, line: string): Promise<void> => {
|
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
|
||||||
try {
|
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
|
||||||
const { response, text, images } = await this.ask("command_output", line)
|
const process = this.terminalManager.runCommand(terminalInfo, command, cwd)
|
||||||
const isStdin = (text ?? "").startsWith(COMMAND_STDIN_STRING)
|
|
||||||
// if this ask promise is not ignored, that means the user responded to it somehow either by clicking primary button or by typing text
|
|
||||||
if (response === "yesButtonTapped") {
|
|
||||||
// SIGINT is typically what's sent when a user interrupts a process (like pressing Ctrl+C)
|
|
||||||
/*
|
|
||||||
.kill sends SIGINT by default. However by not passing any options into .kill(), execa internally sends a SIGKILL after a grace period if the SIGINT failed.
|
|
||||||
however it turns out that even this isn't enough for certain processes like npm starting servers. therefore we use the tree-kill package to kill all processes in the process tree, including the root process.
|
|
||||||
- Sends signal to all children processes of the process with pid pid, including pid. Signal defaults to SIGTERM.
|
|
||||||
*/
|
|
||||||
if (subprocess.pid) {
|
|
||||||
//subprocess.kill("SIGINT") // will result in for loop throwing error
|
|
||||||
treeKill(subprocess.pid, "SIGINT")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isStdin) {
|
|
||||||
const stdin = text?.slice(COMMAND_STDIN_STRING.length) ?? ""
|
|
||||||
|
|
||||||
// replace last commandoutput with + stdin
|
let userFeedback: { text?: string; images?: string[] } | undefined
|
||||||
const lastCommandOutput = findLastIndex(this.claudeMessages, (m) => m.ask === "command_output")
|
const sendCommandOutput = async (line: string): Promise<void> => {
|
||||||
if (lastCommandOutput !== -1) {
|
try {
|
||||||
this.claudeMessages[lastCommandOutput].text += stdin
|
const { response, text, images } = await this.ask("command_output", line)
|
||||||
}
|
if (response === "yesButtonTapped") {
|
||||||
|
// proceed while running
|
||||||
// if the user sent some input, we send it to the command stdin
|
|
||||||
// add newline as cli programs expect a newline after each input
|
|
||||||
// (stdin needs to be set to `pipe` to send input to the command, execa does this by default when using template literals - other options are inherit (from parent process stdin) or null (no stdin))
|
|
||||||
subprocess.stdin?.write(stdin + "\n")
|
|
||||||
// Recurse with an empty string to continue listening for more input
|
|
||||||
sendCommandOutput(subprocess, "") // empty strings are effectively ignored by the webview, this is done solely to relinquish control over the exit command button
|
|
||||||
} else {
|
} else {
|
||||||
userFeedback = { text, images }
|
userFeedback = { text, images }
|
||||||
if (subprocess.pid) {
|
|
||||||
treeKill(subprocess.pid, "SIGINT")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
process.continue() // continue past the await
|
||||||
|
} catch {
|
||||||
|
// This can only happen if this ask promise was ignored, so ignore this error
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// This can only happen if this ask promise was ignored, so ignore this error
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result = ""
|
let result = ""
|
||||||
// execa by default tries to convert bash into javascript, so need to specify `shell: true` to use sh on unix or cmd.exe on windows
|
process.on("line", (line) => {
|
||||||
// also worth noting that execa`input` and the execa(command) have nuanced differences like the template literal version handles escaping for you, while with the function call, you need to be more careful about how arguments are passed, especially when using shell: true.
|
console.log("sending line from here", line)
|
||||||
// execa returns a promise-like object that is both a promise and a Subprocess that has properties like stdin
|
result += line
|
||||||
const subprocess = execa({ shell: true, cwd: cwd })`${command}`
|
sendCommandOutput(line)
|
||||||
this.executeCommandRunningProcess = subprocess
|
|
||||||
|
|
||||||
subprocess.stdout?.on("data", (data) => {
|
|
||||||
if (data) {
|
|
||||||
const output = data.toString()
|
|
||||||
// stream output to user in realtime
|
|
||||||
// do not await since it's sent as an ask and we are not waiting for a response
|
|
||||||
sendCommandOutput(subprocess, output)
|
|
||||||
result += output
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
await process
|
||||||
await subprocess
|
|
||||||
// NOTE: using for await to stream execa output does not return lines that expect user input, so we use listen to the stdout stream and handle data directly, allowing us to process output as soon as it's available even before a full line is complete.
|
|
||||||
// for await (const chunk of subprocess) {
|
|
||||||
// const line = chunk.toString()
|
|
||||||
// sendCommandOutput(subprocess, line)
|
|
||||||
// result += `${line}\n`
|
|
||||||
// }
|
|
||||||
} catch (e) {
|
|
||||||
if ((e as ExecaError).signal === "SIGINT") {
|
|
||||||
//await this.say("command_output", `\nUser exited command...`)
|
|
||||||
result += `\n====\nUser terminated command process via SIGINT. This is not an error. Please continue with your task, but keep in mind that the command is no longer running. For example, if this command was used to start a server for a react app, the server is no longer running and you cannot open a browser to view it anymore.`
|
|
||||||
} else {
|
|
||||||
throw e // if the command was not terminated by user, let outer catch handle it as a real error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Wait for a short delay to ensure all messages are sent to the webview
|
// Wait for a short delay to ensure all messages are sent to the webview
|
||||||
// This delay allows time for non-awaited promises to be created and
|
// This delay allows time for non-awaited promises to be created and
|
||||||
// for their associated messages to be sent to the webview, maintaining
|
// for their associated messages to be sent to the webview, maintaining
|
||||||
// the correct order of messages (although the webview is smart about
|
// the correct order of messages (although the webview is smart about
|
||||||
// grouping command_output messages despite any gaps anyways)
|
// grouping command_output messages despite any gaps anyways)
|
||||||
await delay(100)
|
await delay(10)
|
||||||
this.executeCommandRunningProcess = undefined
|
|
||||||
|
|
||||||
if (userFeedback) {
|
if (userFeedback) {
|
||||||
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
||||||
@@ -1522,12 +1468,10 @@ export class ClaudeDev {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`
|
return `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
const error = e as any
|
|
||||||
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
|
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
|
||||||
const errorString = `Error executing command:\n${errorMessage}`
|
const errorString = `Error executing command:\n${errorMessage}`
|
||||||
await this.say("error", `Error executing command:\n${errorMessage}`) // TODO: in webview show code block for command errors
|
await this.say("error", `Error executing command:\n${errorMessage}`)
|
||||||
this.executeCommandRunningProcess = undefined
|
|
||||||
return errorString
|
return errorString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
328
src/integrations/TerminalManager.ts
Normal file
328
src/integrations/TerminalManager.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import * as vscode from "vscode"
|
||||||
|
import { EventEmitter } from "events"
|
||||||
|
import delay from "delay"
|
||||||
|
|
||||||
|
/*
|
||||||
|
TerminalManager:
|
||||||
|
- Creates/reuses terminals
|
||||||
|
- Runs commands via runCommand(), returning a TerminalProcess
|
||||||
|
- Handles shell integration events
|
||||||
|
|
||||||
|
TerminalProcess extends EventEmitter and implements Promise:
|
||||||
|
- Emits 'line' events with output while promise is pending
|
||||||
|
- process.continue() resolves promise and stops event emission
|
||||||
|
- Allows real-time output handling or background execution
|
||||||
|
|
||||||
|
getUnretrievedOutput() fetches latest output for ongoing commands
|
||||||
|
|
||||||
|
Enables flexible command execution:
|
||||||
|
- Await for completion
|
||||||
|
- Listen to real-time events
|
||||||
|
- Continue execution in background
|
||||||
|
- Retrieve missed output later
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
const terminalManager = new TerminalManager(context);
|
||||||
|
|
||||||
|
// Run a command
|
||||||
|
const process = terminalManager.runCommand('npm install', '/path/to/project');
|
||||||
|
|
||||||
|
process.on('line', (line) => {
|
||||||
|
console.log(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
// To wait for the process to complete naturally:
|
||||||
|
await process;
|
||||||
|
|
||||||
|
// Or to continue execution even if the command is still running:
|
||||||
|
process.continue();
|
||||||
|
|
||||||
|
// Later, if you need to get the unretrieved output:
|
||||||
|
const unretrievedOutput = terminalManager.getUnretrievedOutput(terminalId);
|
||||||
|
console.log('Unretrieved output:', unretrievedOutput);
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class TerminalManager {
|
||||||
|
private static readonly TERMINAL_NAME = "Claude Dev"
|
||||||
|
private terminals: TerminalInfo[] = []
|
||||||
|
private processes: Map<number, TerminalProcess> = new Map()
|
||||||
|
private context: vscode.ExtensionContext
|
||||||
|
private nextTerminalId = 1
|
||||||
|
|
||||||
|
constructor(context: vscode.ExtensionContext) {
|
||||||
|
this.context = context
|
||||||
|
this.setupListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupListeners() {
|
||||||
|
// todo: make sure we do this check everywhere we use the new terminal APIs
|
||||||
|
if (hasShellIntegrationApis()) {
|
||||||
|
this.context.subscriptions.push(
|
||||||
|
vscode.window.onDidOpenTerminal(this.handleOpenTerminal.bind(this)),
|
||||||
|
vscode.window.onDidCloseTerminal(this.handleClosedTerminal.bind(this)),
|
||||||
|
vscode.window.onDidChangeTerminalShellIntegration(this.handleShellIntegrationChange.bind(this)),
|
||||||
|
vscode.window.onDidStartTerminalShellExecution(this.handleShellExecutionStart.bind(this)),
|
||||||
|
vscode.window.onDidEndTerminalShellExecution(this.handleShellExecutionEnd.bind(this))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runCommand(terminalInfo: TerminalInfo, command: string, cwd: string): TerminalProcessResultPromise {
|
||||||
|
terminalInfo.busy = true
|
||||||
|
terminalInfo.lastCommand = command
|
||||||
|
|
||||||
|
const process = new TerminalProcess(terminalInfo, command)
|
||||||
|
|
||||||
|
this.processes.set(terminalInfo.id, process)
|
||||||
|
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
process.once(CONTINUE_EVENT, () => {
|
||||||
|
console.log("2")
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
process.once("error", reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
// if shell integration is already active, run the command immediately
|
||||||
|
if (terminalInfo.terminal.shellIntegration) {
|
||||||
|
process.waitForShellIntegration = false
|
||||||
|
process.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasShellIntegrationApis()) {
|
||||||
|
// Fallback to sendText if there is no shell integration within 3 seconds of launching (could be because the user is not running one of the supported shells)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!terminalInfo.terminal.shellIntegration) {
|
||||||
|
process.waitForShellIntegration = false
|
||||||
|
process.run()
|
||||||
|
// Without shell integration, we can't know when the command has finished or what the
|
||||||
|
// exit code was.
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
} else {
|
||||||
|
// User doesn't have shell integration API available, run command the old way
|
||||||
|
process.waitForShellIntegration = false
|
||||||
|
process.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the process and promise
|
||||||
|
return mergePromise(process, promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrCreateTerminal(cwd: string): Promise<TerminalInfo> {
|
||||||
|
const availableTerminal = this.terminals.find((t) => {
|
||||||
|
if (t.busy) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const terminalCwd = t.terminal.shellIntegration?.cwd // one of claude's commands could have changed the cwd of the terminal
|
||||||
|
if (!terminalCwd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return vscode.Uri.file(cwd).fsPath === terminalCwd.fsPath
|
||||||
|
})
|
||||||
|
if (availableTerminal) {
|
||||||
|
console.log("reusing terminal", availableTerminal.id)
|
||||||
|
return availableTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTerminal = vscode.window.createTerminal({
|
||||||
|
name: `${TerminalManager.TERMINAL_NAME} ${this.nextTerminalId}`,
|
||||||
|
cwd: cwd,
|
||||||
|
})
|
||||||
|
const newTerminalInfo: TerminalInfo = {
|
||||||
|
terminal: newTerminal,
|
||||||
|
busy: false,
|
||||||
|
lastCommand: "",
|
||||||
|
id: this.nextTerminalId++,
|
||||||
|
}
|
||||||
|
this.terminals.push(newTerminalInfo)
|
||||||
|
return newTerminalInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOpenTerminal(terminal: vscode.Terminal) {
|
||||||
|
console.log(`Terminal opened: ${terminal.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClosedTerminal(terminal: vscode.Terminal) {
|
||||||
|
const index = this.terminals.findIndex((t) => t.terminal === terminal)
|
||||||
|
if (index !== -1) {
|
||||||
|
const terminalInfo = this.terminals[index]
|
||||||
|
this.terminals.splice(index, 1)
|
||||||
|
this.processes.delete(terminalInfo.id)
|
||||||
|
}
|
||||||
|
console.log(`Terminal closed: ${terminal.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleShellIntegrationChange(e: vscode.TerminalShellIntegrationChangeEvent) {
|
||||||
|
const terminalInfo = this.terminals.find((t) => t.terminal === e.terminal)
|
||||||
|
if (terminalInfo) {
|
||||||
|
const process = this.processes.get(terminalInfo.id)
|
||||||
|
if (process && process.waitForShellIntegration) {
|
||||||
|
process.waitForShellIntegration = false
|
||||||
|
process.run()
|
||||||
|
}
|
||||||
|
console.log(`Shell integration activated for terminal: ${e.terminal.name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleShellExecutionStart(e: vscode.TerminalShellExecutionStartEvent) {
|
||||||
|
const terminalInfo = this.terminals.find((t) => t.terminal === e.terminal)
|
||||||
|
if (terminalInfo) {
|
||||||
|
terminalInfo.busy = true
|
||||||
|
terminalInfo.lastCommand = e.execution.commandLine.value
|
||||||
|
console.log(`Command started in terminal ${terminalInfo.id}: ${terminalInfo.lastCommand}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleShellExecutionEnd(e: vscode.TerminalShellExecutionEndEvent) {
|
||||||
|
const terminalInfo = this.terminals.find((t) => t.terminal === e.terminal)
|
||||||
|
if (terminalInfo) {
|
||||||
|
this.handleCommandCompletion(terminalInfo, e.exitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCommandCompletion(terminalInfo: TerminalInfo, exitCode?: number | undefined) {
|
||||||
|
terminalInfo.busy = false
|
||||||
|
console.log(
|
||||||
|
`Command "${terminalInfo.lastCommand}" in terminal ${terminalInfo.id} completed with exit code: ${exitCode}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getBusyTerminals(): { id: number; lastCommand: string }[] {
|
||||||
|
return this.terminals.filter((t) => t.busy).map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
|
||||||
|
}
|
||||||
|
|
||||||
|
hasBusyTerminals(): boolean {
|
||||||
|
return this.terminals.some((t) => t.busy)
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnretrievedOutput(terminalId: number): string {
|
||||||
|
const process = this.processes.get(terminalId)
|
||||||
|
if (!process) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return process.getUnretrievedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeAll() {
|
||||||
|
for (const info of this.terminals) {
|
||||||
|
info.terminal.dispose() // todo do we want to do this? test with tab view closing it
|
||||||
|
}
|
||||||
|
this.terminals = []
|
||||||
|
this.processes.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasShellIntegrationApis(): boolean {
|
||||||
|
const [major, minor] = vscode.version.split(".").map(Number)
|
||||||
|
return major > 1 || (major === 1 && minor >= 93)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TerminalInfo {
|
||||||
|
terminal: vscode.Terminal
|
||||||
|
busy: boolean
|
||||||
|
lastCommand: string
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONTINUE_EVENT = "CONTINUE_EVENT"
|
||||||
|
|
||||||
|
export class TerminalProcess extends EventEmitter {
|
||||||
|
waitForShellIntegration: boolean = true
|
||||||
|
private isListening: boolean = true
|
||||||
|
private buffer: string = ""
|
||||||
|
private execution?: vscode.TerminalShellExecution
|
||||||
|
private stream?: AsyncIterable<string>
|
||||||
|
private fullOutput: string = ""
|
||||||
|
private lastRetrievedIndex: number = 0
|
||||||
|
|
||||||
|
constructor(public terminalInfo: TerminalInfo, private command: string) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
if (this.terminalInfo.terminal.shellIntegration) {
|
||||||
|
this.execution = this.terminalInfo.terminal.shellIntegration.executeCommand(this.command)
|
||||||
|
this.stream = this.execution.read()
|
||||||
|
// todo: need to handle errors
|
||||||
|
let isFirstChunk = true // ignore first chunk since it's vscode shell integration marker
|
||||||
|
for await (const data of this.stream) {
|
||||||
|
console.log("data", data)
|
||||||
|
if (!isFirstChunk) {
|
||||||
|
this.fullOutput += data
|
||||||
|
if (this.isListening) {
|
||||||
|
this.emitIfEol(data)
|
||||||
|
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isFirstChunk = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit any remaining content in the buffer
|
||||||
|
if (this.buffer && this.isListening) {
|
||||||
|
this.emit("line", this.buffer.trim())
|
||||||
|
this.buffer = ""
|
||||||
|
this.lastRetrievedIndex = this.fullOutput.length
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit(CONTINUE_EVENT)
|
||||||
|
} else {
|
||||||
|
this.terminalInfo.terminal.sendText(this.command, true)
|
||||||
|
// For terminals without shell integration, we can't know when the command completes
|
||||||
|
// So we'll just emit the continue event after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emit(CONTINUE_EVENT)
|
||||||
|
}, 2000) // Adjust this delay as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspired by https://github.com/sindresorhus/execa/blob/main/lib/transform/split.js
|
||||||
|
private emitIfEol(chunk: string) {
|
||||||
|
this.buffer += chunk
|
||||||
|
let lineEndIndex: number
|
||||||
|
while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
|
||||||
|
let line = this.buffer.slice(0, lineEndIndex).trim()
|
||||||
|
// Remove \r if present (for Windows-style line endings)
|
||||||
|
// if (line.endsWith("\r")) {
|
||||||
|
// line = line.slice(0, -1)
|
||||||
|
// }
|
||||||
|
this.emit("line", line)
|
||||||
|
this.buffer = this.buffer.slice(lineEndIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue() {
|
||||||
|
this.isListening = false
|
||||||
|
this.removeAllListeners("line")
|
||||||
|
this.emit(CONTINUE_EVENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
isStillListening() {
|
||||||
|
return this.isListening
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnretrievedOutput(): string {
|
||||||
|
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
|
||||||
|
this.lastRetrievedIndex = this.fullOutput.length
|
||||||
|
return unretrieved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TerminalProcessResultPromise = TerminalProcess & Promise<void>
|
||||||
|
|
||||||
|
// Similar to execa's ResultPromise, this lets us create a mixin of both a TerminalProcess and a Promise: https://github.com/sindresorhus/execa/blob/main/lib/methods/promise.js
|
||||||
|
function mergePromise(process: TerminalProcess, promise: Promise<void>): TerminalProcessResultPromise {
|
||||||
|
const nativePromisePrototype = (async () => {})().constructor.prototype
|
||||||
|
const descriptors = ["then", "catch", "finally"].map(
|
||||||
|
(property) => [property, Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property)] as const
|
||||||
|
)
|
||||||
|
for (const [property, descriptor] of descriptors) {
|
||||||
|
if (descriptor) {
|
||||||
|
const value = descriptor.value.bind(promise)
|
||||||
|
Reflect.defineProperty(process, property, { ...descriptor, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return process as TerminalProcessResultPromise
|
||||||
|
}
|
||||||
@@ -71,4 +71,3 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const COMMAND_OUTPUT_STRING = "Output:"
|
export const COMMAND_OUTPUT_STRING = "Output:"
|
||||||
export const COMMAND_STDIN_STRING = "Input:"
|
|
||||||
|
|||||||
1
webview-ui/package-lock.json
generated
1
webview-ui/package-lock.json
generated
@@ -27,7 +27,6 @@
|
|||||||
"react-virtuoso": "^4.7.13",
|
"react-virtuoso": "^4.7.13",
|
||||||
"rehype-highlight": "^7.0.0",
|
"rehype-highlight": "^7.0.0",
|
||||||
"rewire": "^7.0.0",
|
"rewire": "^7.0.0",
|
||||||
"strip-ansi": "^7.1.0",
|
|
||||||
"styled-components": "^6.1.13",
|
"styled-components": "^6.1.13",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
"react-virtuoso": "^4.7.13",
|
"react-virtuoso": "^4.7.13",
|
||||||
"rehype-highlight": "^7.0.0",
|
"rehype-highlight": "^7.0.0",
|
||||||
"rewire": "^7.0.0",
|
"rewire": "^7.0.0",
|
||||||
"strip-ansi": "^7.1.0",
|
|
||||||
"styled-components": "^6.1.13",
|
"styled-components": "^6.1.13",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||||
|
import deepEqual from "fast-deep-equal"
|
||||||
import React, { memo, useMemo } from "react"
|
import React, { memo, useMemo } from "react"
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
||||||
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
||||||
import CodeAccordian from "./CodeAccordian"
|
import CodeAccordian from "./CodeAccordian"
|
||||||
import CodeBlock from "./CodeBlock"
|
import CodeBlock from "./CodeBlock"
|
||||||
import Terminal from "./Terminal"
|
|
||||||
import Thumbnails from "./Thumbnails"
|
import Thumbnails from "./Thumbnails"
|
||||||
import deepEqual from "fast-deep-equal"
|
|
||||||
|
|
||||||
interface ChatRowProps {
|
interface ChatRowProps {
|
||||||
message: ClaudeMessage
|
message: ClaudeMessage
|
||||||
@@ -15,7 +14,6 @@ interface ChatRowProps {
|
|||||||
onToggleExpand: () => void
|
onToggleExpand: () => void
|
||||||
lastModifiedMessage?: ClaudeMessage
|
lastModifiedMessage?: ClaudeMessage
|
||||||
isLast: boolean
|
isLast: boolean
|
||||||
handleSendStdin: (text: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatRow = memo(
|
const ChatRow = memo(
|
||||||
@@ -36,14 +34,7 @@ const ChatRow = memo(
|
|||||||
|
|
||||||
export default ChatRow
|
export default ChatRow
|
||||||
|
|
||||||
const ChatRowContent = ({
|
const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowProps) => {
|
||||||
message,
|
|
||||||
isExpanded,
|
|
||||||
onToggleExpand,
|
|
||||||
lastModifiedMessage,
|
|
||||||
isLast,
|
|
||||||
handleSendStdin,
|
|
||||||
}: ChatRowProps) => {
|
|
||||||
const cost = useMemo(() => {
|
const cost = useMemo(() => {
|
||||||
if (message.text != null && message.say === "api_req_started") {
|
if (message.text != null && message.say === "api_req_started") {
|
||||||
return JSON.parse(message.text).cost
|
return JSON.parse(message.text).cost
|
||||||
@@ -483,7 +474,29 @@ const ChatRowContent = ({
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
command: text.slice(0, outputIndex).trim(),
|
command: text.slice(0, outputIndex).trim(),
|
||||||
output: text.slice(outputIndex + COMMAND_OUTPUT_STRING.length).trim() + " ",
|
output: text
|
||||||
|
.slice(outputIndex + COMMAND_OUTPUT_STRING.length)
|
||||||
|
.trim()
|
||||||
|
.split("")
|
||||||
|
.map((char) => {
|
||||||
|
switch (char) {
|
||||||
|
case "\n":
|
||||||
|
return "↵\n"
|
||||||
|
case "\r":
|
||||||
|
return "⏎"
|
||||||
|
case "\t":
|
||||||
|
return "→ "
|
||||||
|
case "\b":
|
||||||
|
return "⌫"
|
||||||
|
case "\f":
|
||||||
|
return "⏏"
|
||||||
|
case "\v":
|
||||||
|
return "⇳"
|
||||||
|
default:
|
||||||
|
return char
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,11 +507,28 @@ const ChatRowContent = ({
|
|||||||
{icon}
|
{icon}
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<Terminal
|
{/* <Terminal
|
||||||
rawOutput={command + (output ? "\n" + output : "")}
|
rawOutput={command + (output ? "\n" + output : "")}
|
||||||
handleSendStdin={handleSendStdin}
|
|
||||||
shouldAllowInput={!!isCommandExecuting && output.length > 0}
|
shouldAllowInput={!!isCommandExecuting && output.length > 0}
|
||||||
/>
|
/> */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: 3,
|
||||||
|
border: "1px solid var(--vscode-sideBar-border)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}>
|
||||||
|
<CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} />
|
||||||
|
</div>
|
||||||
|
{output.length > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: 3,
|
||||||
|
border: "1px solid var(--vscode-sideBar-border)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}>
|
||||||
|
<CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case "completion_result":
|
case "completion_result":
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useEvent, useMount } from "react-use"
|
|||||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||||
import { ClaudeAsk, ClaudeSayTool, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
|
import { ClaudeAsk, ClaudeSayTool, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
import { combineApiRequests } from "../../../src/shared/combineApiRequests"
|
import { combineApiRequests } from "../../../src/shared/combineApiRequests"
|
||||||
import { combineCommandSequences, COMMAND_STDIN_STRING } from "../../../src/shared/combineCommandSequences"
|
import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
|
||||||
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
|
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
|
||||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
@@ -118,7 +118,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setTextAreaDisabled(false)
|
setTextAreaDisabled(false)
|
||||||
setClaudeAsk("command_output")
|
setClaudeAsk("command_output")
|
||||||
setEnableButtons(true)
|
setEnableButtons(true)
|
||||||
setPrimaryButtonText("Exit Command")
|
setPrimaryButtonText("Proceed While Running")
|
||||||
setSecondaryButtonText(undefined)
|
setSecondaryButtonText(undefined)
|
||||||
break
|
break
|
||||||
case "completion_result":
|
case "completion_result":
|
||||||
@@ -224,23 +224,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
}
|
}
|
||||||
}, [inputValue, selectedImages, messages.length, claudeAsk])
|
}, [inputValue, selectedImages, messages.length, claudeAsk])
|
||||||
|
|
||||||
const handleSendStdin = useCallback(
|
|
||||||
(text: string) => {
|
|
||||||
if (claudeAsk === "command_output") {
|
|
||||||
vscode.postMessage({
|
|
||||||
type: "askResponse",
|
|
||||||
askResponse: "messageResponse",
|
|
||||||
text: COMMAND_STDIN_STRING + text,
|
|
||||||
})
|
|
||||||
setClaudeAsk(undefined)
|
|
||||||
// don't need to disable since extension relinquishes control back immediately
|
|
||||||
// setTextAreaDisabled(true)
|
|
||||||
// setEnableButtons(false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[claudeAsk]
|
|
||||||
)
|
|
||||||
|
|
||||||
const startNewTask = useCallback(() => {
|
const startNewTask = useCallback(() => {
|
||||||
vscode.postMessage({ type: "clearTask" })
|
vscode.postMessage({ type: "clearTask" })
|
||||||
}, [])
|
}, [])
|
||||||
@@ -468,10 +451,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
||||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||||
isLast={index === visibleMessages.length - 1}
|
isLast={index === visibleMessages.length - 1}
|
||||||
handleSendStdin={handleSendStdin}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[expandedRows, modifiedMessages, visibleMessages.length, handleSendStdin]
|
[expandedRows, modifiedMessages, visibleMessages.length]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,351 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo, memo } from "react"
|
|
||||||
import DynamicTextArea from "react-textarea-autosize"
|
|
||||||
import stripAnsi from "strip-ansi"
|
|
||||||
|
|
||||||
interface TerminalProps {
|
|
||||||
rawOutput: string
|
|
||||||
handleSendStdin: (text: string) => void
|
|
||||||
shouldAllowInput: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Inspired by https://phuoc.ng/collection/mirror-a-text-area/create-your-own-custom-cursor-in-a-text-area/
|
|
||||||
|
|
||||||
Note: Even though vscode exposes var(--vscode-terminalCursor-foreground) it does not render in front of a color that isn't var(--vscode-terminal-background), and it turns out a lot of themes don't even define some/any of these terminal color variables. Very odd behavior, so try changing themes/color variables if you don't see the caret.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Terminal = ({ rawOutput, handleSendStdin, shouldAllowInput }: TerminalProps) => {
|
|
||||||
const [userInput, setUserInput] = useState("")
|
|
||||||
const [isFocused, setIsFocused] = useState(false) // Initially not focused
|
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
|
||||||
const mirrorRef = useRef<HTMLDivElement>(null)
|
|
||||||
const hiddenTextareaRef = useRef<HTMLTextAreaElement>(null)
|
|
||||||
|
|
||||||
const [lastProcessedOutput, setLastProcessedOutput] = useState("")
|
|
||||||
|
|
||||||
const output = useMemo(() => {
|
|
||||||
return stripAnsi(rawOutput)
|
|
||||||
}, [rawOutput])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (lastProcessedOutput !== output) {
|
|
||||||
setUserInput("")
|
|
||||||
}
|
|
||||||
}, [output, lastProcessedOutput])
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSendStdin(userInput)
|
|
||||||
// setUserInput("") // Clear user input after processing
|
|
||||||
setLastProcessedOutput(output)
|
|
||||||
|
|
||||||
// Trigger resize after clearing input
|
|
||||||
const textarea = textAreaRef.current
|
|
||||||
const hiddenTextarea = hiddenTextareaRef.current
|
|
||||||
if (textarea && hiddenTextarea) {
|
|
||||||
hiddenTextarea.value = ""
|
|
||||||
const newHeight = hiddenTextarea.scrollHeight
|
|
||||||
textarea.style.height = `${newHeight}px`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setUserInput("") // Reset user input when output changes
|
|
||||||
}, [output])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const textarea = textAreaRef.current
|
|
||||||
const mirror = mirrorRef.current
|
|
||||||
const hiddenTextarea = hiddenTextareaRef.current
|
|
||||||
if (!textarea || !mirror || !hiddenTextarea) return
|
|
||||||
|
|
||||||
const textareaStyles = window.getComputedStyle(textarea)
|
|
||||||
const stylesToCopy = [
|
|
||||||
"border",
|
|
||||||
"boxSizing",
|
|
||||||
"fontFamily",
|
|
||||||
"fontSize",
|
|
||||||
"fontWeight",
|
|
||||||
"letterSpacing",
|
|
||||||
"lineHeight",
|
|
||||||
"padding",
|
|
||||||
"textDecoration",
|
|
||||||
"textIndent",
|
|
||||||
"textTransform",
|
|
||||||
"whiteSpace",
|
|
||||||
"wordSpacing",
|
|
||||||
"wordWrap",
|
|
||||||
"width",
|
|
||||||
"height",
|
|
||||||
]
|
|
||||||
|
|
||||||
stylesToCopy.forEach((property) => {
|
|
||||||
mirror.style[property as any] = textareaStyles[property as any]
|
|
||||||
hiddenTextarea.style[property as any] = textareaStyles[property as any]
|
|
||||||
})
|
|
||||||
mirror.style.borderColor = "transparent"
|
|
||||||
hiddenTextarea.style.visibility = "hidden"
|
|
||||||
hiddenTextarea.style.position = "absolute"
|
|
||||||
// hiddenTextarea.style.height = "auto"
|
|
||||||
hiddenTextarea.style.width = `${textarea.clientWidth}px`
|
|
||||||
hiddenTextarea.style.whiteSpace = "pre-wrap"
|
|
||||||
hiddenTextarea.style.overflowWrap = "break-word"
|
|
||||||
|
|
||||||
// const borderWidth = parseInt(textareaStyles.borderWidth, 10) || 0
|
|
||||||
const updateSize = () => {
|
|
||||||
hiddenTextarea.value = textarea.value
|
|
||||||
const newHeight = hiddenTextarea.scrollHeight
|
|
||||||
textarea.style.height = `${newHeight}px`
|
|
||||||
mirror.style.width = `${textarea.offsetWidth}px`
|
|
||||||
mirror.style.height = `${newHeight}px`
|
|
||||||
hiddenTextarea.style.width = `${textarea.offsetWidth}px`
|
|
||||||
hiddenTextarea.style.height = `${newHeight}px`
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize()
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(updateSize)
|
|
||||||
resizeObserver.observe(textarea)
|
|
||||||
|
|
||||||
// Add window resize event listener
|
|
||||||
const handleWindowResize = () => {
|
|
||||||
hiddenTextarea.style.width = `${textarea.clientWidth}px`
|
|
||||||
updateSize()
|
|
||||||
}
|
|
||||||
window.addEventListener("resize", handleWindowResize)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
window.removeEventListener("resize", handleWindowResize)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const textarea = textAreaRef.current
|
|
||||||
const mirror = mirrorRef.current
|
|
||||||
if (!textarea || !mirror) return
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (mirror) mirror.scrollTop = textarea.scrollTop
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.addEventListener("scroll", handleScroll)
|
|
||||||
return () => textarea.removeEventListener("scroll", handleScroll)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const textarea = textAreaRef.current
|
|
||||||
const mirror = mirrorRef.current
|
|
||||||
if (!textarea || !mirror) return
|
|
||||||
|
|
||||||
const updateMirror = () => {
|
|
||||||
const cursorPos = textarea.selectionStart
|
|
||||||
const textBeforeCursor = textarea.value.substring(0, cursorPos)
|
|
||||||
const textAfterCursor = textarea.value.substring(cursorPos)
|
|
||||||
|
|
||||||
mirror.innerHTML = ""
|
|
||||||
mirror.appendChild(document.createTextNode(textBeforeCursor))
|
|
||||||
|
|
||||||
const caretEle = document.createElement("span")
|
|
||||||
caretEle.classList.add("terminal-cursor")
|
|
||||||
if (isFocused) {
|
|
||||||
caretEle.classList.add("terminal-cursor-focused")
|
|
||||||
}
|
|
||||||
if (!shouldAllowInput) {
|
|
||||||
caretEle.classList.add("terminal-cursor-hidden")
|
|
||||||
}
|
|
||||||
caretEle.innerHTML = " "
|
|
||||||
mirror.appendChild(caretEle)
|
|
||||||
|
|
||||||
mirror.appendChild(document.createTextNode(textAfterCursor))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update mirror on initial render
|
|
||||||
updateMirror()
|
|
||||||
|
|
||||||
document.addEventListener("selectionchange", updateMirror)
|
|
||||||
return () => document.removeEventListener("selectionchange", updateMirror)
|
|
||||||
}, [userInput, isFocused, shouldAllowInput])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Position the dummy caret at the end of the text on initial render
|
|
||||||
const mirror = mirrorRef.current
|
|
||||||
if (mirror) {
|
|
||||||
const text = output + userInput
|
|
||||||
mirror.innerHTML = ""
|
|
||||||
mirror.appendChild(document.createTextNode(text))
|
|
||||||
|
|
||||||
const caretEle = document.createElement("span")
|
|
||||||
caretEle.classList.add("terminal-cursor")
|
|
||||||
if (isFocused) {
|
|
||||||
caretEle.classList.add("terminal-cursor-focused")
|
|
||||||
}
|
|
||||||
if (!shouldAllowInput) {
|
|
||||||
caretEle.classList.add("terminal-cursor-hidden")
|
|
||||||
}
|
|
||||||
caretEle.innerHTML = " "
|
|
||||||
mirror.appendChild(caretEle)
|
|
||||||
}
|
|
||||||
}, [output, userInput, isFocused, shouldAllowInput])
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const newValue = e.target.value
|
|
||||||
|
|
||||||
// Ensure the user can only edit their input after the output
|
|
||||||
if (newValue.startsWith(output)) {
|
|
||||||
setUserInput(newValue.slice(output.length))
|
|
||||||
} else {
|
|
||||||
// If the user tries to edit the output part, reset the value to the correct state
|
|
||||||
e.target.value = output + userInput
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger resize after setting user input
|
|
||||||
const textarea = textAreaRef.current
|
|
||||||
const hiddenTextarea = hiddenTextareaRef.current
|
|
||||||
if (textarea && hiddenTextarea) {
|
|
||||||
hiddenTextarea.value = output + userInput
|
|
||||||
const newHeight = hiddenTextarea.scrollHeight
|
|
||||||
textarea.style.height = `${newHeight}px`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
const textarea = e.target as HTMLTextAreaElement
|
|
||||||
const cursorPosition = textarea.selectionStart
|
|
||||||
|
|
||||||
// Prevent backspace from deleting the output part
|
|
||||||
if (e.key === "Backspace" && cursorPosition <= output.length) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cursor position on backspace
|
|
||||||
setTimeout(() => {
|
|
||||||
const cursorPos = textarea.selectionStart
|
|
||||||
const textBeforeCursor = textarea.value.substring(0, cursorPos)
|
|
||||||
const textAfterCursor = textarea.value.substring(cursorPos)
|
|
||||||
|
|
||||||
mirrorRef.current!.innerHTML = ""
|
|
||||||
mirrorRef.current!.appendChild(document.createTextNode(textBeforeCursor))
|
|
||||||
|
|
||||||
const caretEle = document.createElement("span")
|
|
||||||
caretEle.classList.add("terminal-cursor")
|
|
||||||
if (isFocused) {
|
|
||||||
caretEle.classList.add("terminal-cursor-focused")
|
|
||||||
}
|
|
||||||
if (!shouldAllowInput) {
|
|
||||||
caretEle.classList.add("terminal-cursor-hidden")
|
|
||||||
}
|
|
||||||
caretEle.innerHTML = " "
|
|
||||||
mirrorRef.current!.appendChild(caretEle)
|
|
||||||
|
|
||||||
mirrorRef.current!.appendChild(document.createTextNode(textAfterCursor))
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const textAreaStyle: React.CSSProperties = {
|
|
||||||
fontFamily: "var(--vscode-editor-font-family)",
|
|
||||||
fontSize: "var(--vscode-editor-font-size)",
|
|
||||||
padding: "10px",
|
|
||||||
border: "1px solid var(--vscode-editorGroup-border)",
|
|
||||||
outline: "none",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "100%",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
resize: "none",
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="terminal-container">
|
|
||||||
<style>
|
|
||||||
{`
|
|
||||||
.terminal-container {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden; // Add this
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-textarea {
|
|
||||||
background: transparent;
|
|
||||||
caret-color: transparent;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-mirror {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
color: transparent;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-cursor {
|
|
||||||
border: 1px solid var(--vscode-terminal-foreground, #FFFFFF);
|
|
||||||
position: absolute;
|
|
||||||
width: 4px;
|
|
||||||
margin-top: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-cursor-focused {
|
|
||||||
background-color: var(--vscode-terminal-foreground, #FFFFFF);
|
|
||||||
animation: blink 1s step-end infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-cursor-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<DynamicTextArea
|
|
||||||
ref={textAreaRef}
|
|
||||||
value={output + (shouldAllowInput ? userInput : "")}
|
|
||||||
onChange={handleChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
onFocus={() => setIsFocused(true)}
|
|
||||||
onBlur={() => setIsFocused(false)}
|
|
||||||
className="terminal-textarea"
|
|
||||||
style={{
|
|
||||||
// backgroundColor: "var(--vscode-editor-background)", // NOTE: adding cursor ontop of this color wouldnt work on some themes
|
|
||||||
caretColor: "transparent", // Hide default caret
|
|
||||||
color: "var(--vscode-terminal-foreground)",
|
|
||||||
borderRadius: "3px",
|
|
||||||
...(textAreaStyle as any),
|
|
||||||
}}
|
|
||||||
minRows={1}
|
|
||||||
/>
|
|
||||||
<div ref={mirrorRef} className="terminal-mirror"></div>
|
|
||||||
<DynamicTextArea
|
|
||||||
ref={hiddenTextareaRef}
|
|
||||||
className="terminal-textarea"
|
|
||||||
aria-hidden="true"
|
|
||||||
tabIndex={-1}
|
|
||||||
readOnly
|
|
||||||
minRows={1}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
opacity: 0,
|
|
||||||
...(textAreaStyle as any),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(Terminal)
|
|
||||||
Reference in New Issue
Block a user