From 144fdf16ca68f52416bed838d8ddd77bafcecb52 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 30 Oct 2025 19:08:43 -0400 Subject: [PATCH] add terminal output dropdown, reveal command on focus (#273175) --- package-lock.json | 96 ++-- package.json | 20 +- remote/package-lock.json | 96 ++-- remote/package.json | 20 +- remote/web/package-lock.json | 88 ++-- remote/web/package.json | 18 +- .../commandDetection/terminalCommand.ts | 4 +- .../chat/browser/chatAttachmentWidgets.ts | 4 +- .../media/chatTerminalToolProgressPart.css | 44 ++ .../chatTerminalToolProgressPart.ts | 409 ++++++++++++++++-- .../contrib/chat/browser/media/chat.css | 10 + .../contrib/chat/common/chatService.ts | 5 + .../contrib/chat/common/constants.ts | 2 + .../contrib/terminal/browser/terminal.ts | 11 +- .../terminal/browser/xterm/xtermTerminal.ts | 59 +++ .../chat/browser/terminalChatService.ts | 65 ++- .../browser/tools/runInTerminalTool.ts | 79 +++- 17 files changed, 816 insertions(+), 214 deletions(-) diff --git a/package-lock.json b/package-lock.json index 75a9b72de7e..be209b18375 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,16 +27,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.118", - "@xterm/addon-image": "^0.9.0-beta.135", - "@xterm/addon-ligatures": "^0.10.0-beta.135", - "@xterm/addon-progress": "^0.2.0-beta.41", - "@xterm/addon-search": "^0.16.0-beta.135", - "@xterm/addon-serialize": "^0.14.0-beta.135", - "@xterm/addon-unicode11": "^0.9.0-beta.135", - "@xterm/addon-webgl": "^0.19.0-beta.135", - "@xterm/headless": "^5.6.0-beta.135", - "@xterm/xterm": "^5.6.0-beta.135", + "@xterm/addon-clipboard": "^0.2.0-beta.119", + "@xterm/addon-image": "^0.9.0-beta.136", + "@xterm/addon-ligatures": "^0.10.0-beta.136", + "@xterm/addon-progress": "^0.2.0-beta.42", + "@xterm/addon-search": "^0.16.0-beta.136", + "@xterm/addon-serialize": "^0.14.0-beta.136", + "@xterm/addon-unicode11": "^0.9.0-beta.136", + "@xterm/addon-webgl": "^0.19.0-beta.136", + "@xterm/headless": "^5.6.0-beta.136", + "@xterm/xterm": "^5.6.0-beta.136", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3619,30 +3619,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.118", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.118.tgz", - "integrity": "sha512-wfy/o2PxSKMLy4o75J4RVd1P2W27oa/b+Ay8Z3cq3rWZJPx3onbO8PwAJygJaBxQWmOJ0KD6fxQ99plnd2RdVw==", + "version": "0.2.0-beta.119", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.119.tgz", + "integrity": "sha512-yWmCpGuTvSaIeEfdSijdf8K8qRAYuEGnKkaJZ6er+cOzdmGHBNzyBDKKeyins0aV2j4CGKPDiWHQF5+qGzZDGw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.135.tgz", - "integrity": "sha512-a3LLZitBX4ybuhEtPhSW4dAPqVnnA++pCrIVNXenQVn737oETsLzL9CW6GMpSk1z8xHLl+Bgjfun+6AKpA3ARw==", + "version": "0.9.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.136.tgz", + "integrity": "sha512-syWhqpFMAcQ1+US0JjFzj0ORokj8hkz2VgXcCCbTfO0cDtpSYYxMNLaY2fpL459rnOFB4olI9Nf9PZdonmBPDw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.135.tgz", - "integrity": "sha512-qT8CrULnTKP6qh4a+r1WHmdvzVD507ZgYx7PVVxL4mC/VJwsdny0sRjoZTE6YQWjdoznfTriD69gDu+ztiY8YQ==", + "version": "0.10.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.136.tgz", + "integrity": "sha512-WkvL7BVdoqpNf8QsH4n37Pu7jEZTiJ+OD4FmLMVavw0euhgG18zzJKNKIYRuKcddR52dT/Q8TrspVJofpL98GQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3652,64 +3652,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.41", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.41.tgz", - "integrity": "sha512-9fgyz86G2JAR2NBcayFse08YoCwQljQCY720raIkGyh3F7iQXs7klNHFUfswg9fLzuO+uQIEh76dxi3NirmCPg==", + "version": "0.2.0-beta.42", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.42.tgz", + "integrity": "sha512-C5w7y6rwSUdRcEiJHFnB2qJI/6DBOi/fJAvTmIpmNZE60cVnrLUuyLmXh6aKbSQ44J6W3PrD5xthb8re3UVUOw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.135.tgz", - "integrity": "sha512-Tk7X0T6AsOcxX9hBjpifHi4iXHrPlvTekroiRnDZEY9S1mdINbUIEphOUwwJxsNuukFsxK1VDIiFsGXsZMNO1Q==", + "version": "0.16.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.136.tgz", + "integrity": "sha512-Y2T/ShQBelmOGy7lup3VEfFF/yXeNkkMXqhGftmjzmwSA+eylFW+92vczMSrckTW++EFvVLR/L5jMXiSw0qOWQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.135.tgz", - "integrity": "sha512-88JtWh9Gm5CRfLtgxCD92JXoz5GyRshhQrythLz83VGI9gZVy9dQnwZxmZ/ZE0G17qzM3Z5yvdfiO70q3cGZHA==", + "version": "0.14.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.136.tgz", + "integrity": "sha512-ursvqITzhZrBQT8XsbOyAQJJKohv33NEm6ToLtMZUmPurBG6KXlVZ9LAPs2YpCBqkifLktSE1GdsofJCpADWuA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.135.tgz", - "integrity": "sha512-IFCCOVVQZ0ZnI7ndy40ZjPGHC64YgSncX9/lUw/py0JXM5bMmeg+VdQvpxsdNlvolkVmU3a26TUbPQeZoi0xzw==", + "version": "0.9.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.136.tgz", + "integrity": "sha512-RwtNbON1uNndrtPCM6qMMElTTpxs7ZLRQVbSm4/BMW6GAt6AbW1RAqwoxMRhbz7VVTux/c3HcKfj3SI1MhqSOw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.135.tgz", - "integrity": "sha512-MuJlcCMlapiVHe1jfk/q2wpciPoW8CCWARjsp38k4hOuGdakyZxtZSUP5iWrIH/OvUh/aHmzIK6Ce6vZ1ObJeg==", + "version": "0.19.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.136.tgz", + "integrity": "sha512-MzVlFKrlgJjKQ6T4/TuamvlvR2FFDvxAPY90lo9u4899k7NNif+M8bBdNea3+bsPMU3fKLhGHoTp0+8MjskaeA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.135.tgz", - "integrity": "sha512-fcBUf8zLaTWUyAcahK48dihb1BQYdWvOH021Xge/LfMvCFzK0gmvSMnBbM5fgOyejbmtNLQe+gXBBKVxSaW2pA==", + "version": "5.6.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.136.tgz", + "integrity": "sha512-3irueWS6Ei+XlTMCuh6ZWj1tBnVvjitDtD4PN+v81RKjaCNO/QN9abGTHQx+651GP291ESwY8ocKThSoQ9yklw==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.135.tgz", - "integrity": "sha512-LjiC1wH6qPGZyiNLajQbqih5K6FjfHY9WAr+RzGnaN0+u+6kjxNRvq8iQSvXRqkPcSQq/GqC94vZYb6EKyXxag==", + "version": "5.6.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.136.tgz", + "integrity": "sha512-cOWfdbPUYjV8qJY0yg/HdJBiq/hl8J2NRma563crQbSveDpuiiKV+T+ZVeGKQ2YZztLCz6h+kox6J7LQcPtpiQ==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { diff --git a/package.json b/package.json index 3f06f400886..8141de41fb8 100644 --- a/package.json +++ b/package.json @@ -88,16 +88,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.118", - "@xterm/addon-image": "^0.9.0-beta.135", - "@xterm/addon-ligatures": "^0.10.0-beta.135", - "@xterm/addon-progress": "^0.2.0-beta.41", - "@xterm/addon-search": "^0.16.0-beta.135", - "@xterm/addon-serialize": "^0.14.0-beta.135", - "@xterm/addon-unicode11": "^0.9.0-beta.135", - "@xterm/addon-webgl": "^0.19.0-beta.135", - "@xterm/headless": "^5.6.0-beta.135", - "@xterm/xterm": "^5.6.0-beta.135", + "@xterm/addon-clipboard": "^0.2.0-beta.119", + "@xterm/addon-image": "^0.9.0-beta.136", + "@xterm/addon-ligatures": "^0.10.0-beta.136", + "@xterm/addon-progress": "^0.2.0-beta.42", + "@xterm/addon-search": "^0.16.0-beta.136", + "@xterm/addon-serialize": "^0.14.0-beta.136", + "@xterm/addon-unicode11": "^0.9.0-beta.136", + "@xterm/addon-webgl": "^0.19.0-beta.136", + "@xterm/headless": "^5.6.0-beta.136", + "@xterm/xterm": "^5.6.0-beta.136", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index bdeffca90b6..ff8d5dab11b 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,16 +20,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.118", - "@xterm/addon-image": "^0.9.0-beta.135", - "@xterm/addon-ligatures": "^0.10.0-beta.135", - "@xterm/addon-progress": "^0.2.0-beta.41", - "@xterm/addon-search": "^0.16.0-beta.135", - "@xterm/addon-serialize": "^0.14.0-beta.135", - "@xterm/addon-unicode11": "^0.9.0-beta.135", - "@xterm/addon-webgl": "^0.19.0-beta.135", - "@xterm/headless": "^5.6.0-beta.135", - "@xterm/xterm": "^5.6.0-beta.135", + "@xterm/addon-clipboard": "^0.2.0-beta.119", + "@xterm/addon-image": "^0.9.0-beta.136", + "@xterm/addon-ligatures": "^0.10.0-beta.136", + "@xterm/addon-progress": "^0.2.0-beta.42", + "@xterm/addon-search": "^0.16.0-beta.136", + "@xterm/addon-serialize": "^0.14.0-beta.136", + "@xterm/addon-unicode11": "^0.9.0-beta.136", + "@xterm/addon-webgl": "^0.19.0-beta.136", + "@xterm/headless": "^5.6.0-beta.136", + "@xterm/xterm": "^5.6.0-beta.136", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -238,30 +238,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.118", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.118.tgz", - "integrity": "sha512-wfy/o2PxSKMLy4o75J4RVd1P2W27oa/b+Ay8Z3cq3rWZJPx3onbO8PwAJygJaBxQWmOJ0KD6fxQ99plnd2RdVw==", + "version": "0.2.0-beta.119", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.119.tgz", + "integrity": "sha512-yWmCpGuTvSaIeEfdSijdf8K8qRAYuEGnKkaJZ6er+cOzdmGHBNzyBDKKeyins0aV2j4CGKPDiWHQF5+qGzZDGw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.135.tgz", - "integrity": "sha512-a3LLZitBX4ybuhEtPhSW4dAPqVnnA++pCrIVNXenQVn737oETsLzL9CW6GMpSk1z8xHLl+Bgjfun+6AKpA3ARw==", + "version": "0.9.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.136.tgz", + "integrity": "sha512-syWhqpFMAcQ1+US0JjFzj0ORokj8hkz2VgXcCCbTfO0cDtpSYYxMNLaY2fpL459rnOFB4olI9Nf9PZdonmBPDw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.135.tgz", - "integrity": "sha512-qT8CrULnTKP6qh4a+r1WHmdvzVD507ZgYx7PVVxL4mC/VJwsdny0sRjoZTE6YQWjdoznfTriD69gDu+ztiY8YQ==", + "version": "0.10.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.136.tgz", + "integrity": "sha512-WkvL7BVdoqpNf8QsH4n37Pu7jEZTiJ+OD4FmLMVavw0euhgG18zzJKNKIYRuKcddR52dT/Q8TrspVJofpL98GQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -271,64 +271,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.41", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.41.tgz", - "integrity": "sha512-9fgyz86G2JAR2NBcayFse08YoCwQljQCY720raIkGyh3F7iQXs7klNHFUfswg9fLzuO+uQIEh76dxi3NirmCPg==", + "version": "0.2.0-beta.42", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.42.tgz", + "integrity": "sha512-C5w7y6rwSUdRcEiJHFnB2qJI/6DBOi/fJAvTmIpmNZE60cVnrLUuyLmXh6aKbSQ44J6W3PrD5xthb8re3UVUOw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.135.tgz", - "integrity": "sha512-Tk7X0T6AsOcxX9hBjpifHi4iXHrPlvTekroiRnDZEY9S1mdINbUIEphOUwwJxsNuukFsxK1VDIiFsGXsZMNO1Q==", + "version": "0.16.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.136.tgz", + "integrity": "sha512-Y2T/ShQBelmOGy7lup3VEfFF/yXeNkkMXqhGftmjzmwSA+eylFW+92vczMSrckTW++EFvVLR/L5jMXiSw0qOWQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.135.tgz", - "integrity": "sha512-88JtWh9Gm5CRfLtgxCD92JXoz5GyRshhQrythLz83VGI9gZVy9dQnwZxmZ/ZE0G17qzM3Z5yvdfiO70q3cGZHA==", + "version": "0.14.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.136.tgz", + "integrity": "sha512-ursvqITzhZrBQT8XsbOyAQJJKohv33NEm6ToLtMZUmPurBG6KXlVZ9LAPs2YpCBqkifLktSE1GdsofJCpADWuA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.135.tgz", - "integrity": "sha512-IFCCOVVQZ0ZnI7ndy40ZjPGHC64YgSncX9/lUw/py0JXM5bMmeg+VdQvpxsdNlvolkVmU3a26TUbPQeZoi0xzw==", + "version": "0.9.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.136.tgz", + "integrity": "sha512-RwtNbON1uNndrtPCM6qMMElTTpxs7ZLRQVbSm4/BMW6GAt6AbW1RAqwoxMRhbz7VVTux/c3HcKfj3SI1MhqSOw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.135.tgz", - "integrity": "sha512-MuJlcCMlapiVHe1jfk/q2wpciPoW8CCWARjsp38k4hOuGdakyZxtZSUP5iWrIH/OvUh/aHmzIK6Ce6vZ1ObJeg==", + "version": "0.19.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.136.tgz", + "integrity": "sha512-MzVlFKrlgJjKQ6T4/TuamvlvR2FFDvxAPY90lo9u4899k7NNif+M8bBdNea3+bsPMU3fKLhGHoTp0+8MjskaeA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.135.tgz", - "integrity": "sha512-fcBUf8zLaTWUyAcahK48dihb1BQYdWvOH021Xge/LfMvCFzK0gmvSMnBbM5fgOyejbmtNLQe+gXBBKVxSaW2pA==", + "version": "5.6.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.136.tgz", + "integrity": "sha512-3irueWS6Ei+XlTMCuh6ZWj1tBnVvjitDtD4PN+v81RKjaCNO/QN9abGTHQx+651GP291ESwY8ocKThSoQ9yklw==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.135.tgz", - "integrity": "sha512-LjiC1wH6qPGZyiNLajQbqih5K6FjfHY9WAr+RzGnaN0+u+6kjxNRvq8iQSvXRqkPcSQq/GqC94vZYb6EKyXxag==", + "version": "5.6.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.136.tgz", + "integrity": "sha512-cOWfdbPUYjV8qJY0yg/HdJBiq/hl8J2NRma563crQbSveDpuiiKV+T+ZVeGKQ2YZztLCz6h+kox6J7LQcPtpiQ==", "license": "MIT" }, "node_modules/agent-base": { diff --git a/remote/package.json b/remote/package.json index 44a825ec29b..6be18987d05 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,16 +15,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.118", - "@xterm/addon-image": "^0.9.0-beta.135", - "@xterm/addon-ligatures": "^0.10.0-beta.135", - "@xterm/addon-progress": "^0.2.0-beta.41", - "@xterm/addon-search": "^0.16.0-beta.135", - "@xterm/addon-serialize": "^0.14.0-beta.135", - "@xterm/addon-unicode11": "^0.9.0-beta.135", - "@xterm/addon-webgl": "^0.19.0-beta.135", - "@xterm/headless": "^5.6.0-beta.135", - "@xterm/xterm": "^5.6.0-beta.135", + "@xterm/addon-clipboard": "^0.2.0-beta.119", + "@xterm/addon-image": "^0.9.0-beta.136", + "@xterm/addon-ligatures": "^0.10.0-beta.136", + "@xterm/addon-progress": "^0.2.0-beta.42", + "@xterm/addon-search": "^0.16.0-beta.136", + "@xterm/addon-serialize": "^0.14.0-beta.136", + "@xterm/addon-unicode11": "^0.9.0-beta.136", + "@xterm/addon-webgl": "^0.19.0-beta.136", + "@xterm/headless": "^5.6.0-beta.136", + "@xterm/xterm": "^5.6.0-beta.136", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index d6b531b8974..68d70b572c6 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,15 +13,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.2.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.118", - "@xterm/addon-image": "^0.9.0-beta.135", - "@xterm/addon-ligatures": "^0.10.0-beta.135", - "@xterm/addon-progress": "^0.2.0-beta.41", - "@xterm/addon-search": "^0.16.0-beta.135", - "@xterm/addon-serialize": "^0.14.0-beta.135", - "@xterm/addon-unicode11": "^0.9.0-beta.135", - "@xterm/addon-webgl": "^0.19.0-beta.135", - "@xterm/xterm": "^5.6.0-beta.135", + "@xterm/addon-clipboard": "^0.2.0-beta.119", + "@xterm/addon-image": "^0.9.0-beta.136", + "@xterm/addon-ligatures": "^0.10.0-beta.136", + "@xterm/addon-progress": "^0.2.0-beta.42", + "@xterm/addon-search": "^0.16.0-beta.136", + "@xterm/addon-serialize": "^0.14.0-beta.136", + "@xterm/addon-unicode11": "^0.9.0-beta.136", + "@xterm/addon-webgl": "^0.19.0-beta.136", + "@xterm/xterm": "^5.6.0-beta.136", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client-umd": "0.2.0", @@ -92,30 +92,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.118", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.118.tgz", - "integrity": "sha512-wfy/o2PxSKMLy4o75J4RVd1P2W27oa/b+Ay8Z3cq3rWZJPx3onbO8PwAJygJaBxQWmOJ0KD6fxQ99plnd2RdVw==", + "version": "0.2.0-beta.119", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.119.tgz", + "integrity": "sha512-yWmCpGuTvSaIeEfdSijdf8K8qRAYuEGnKkaJZ6er+cOzdmGHBNzyBDKKeyins0aV2j4CGKPDiWHQF5+qGzZDGw==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.135.tgz", - "integrity": "sha512-a3LLZitBX4ybuhEtPhSW4dAPqVnnA++pCrIVNXenQVn737oETsLzL9CW6GMpSk1z8xHLl+Bgjfun+6AKpA3ARw==", + "version": "0.9.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.136.tgz", + "integrity": "sha512-syWhqpFMAcQ1+US0JjFzj0ORokj8hkz2VgXcCCbTfO0cDtpSYYxMNLaY2fpL459rnOFB4olI9Nf9PZdonmBPDw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.135.tgz", - "integrity": "sha512-qT8CrULnTKP6qh4a+r1WHmdvzVD507ZgYx7PVVxL4mC/VJwsdny0sRjoZTE6YQWjdoznfTriD69gDu+ztiY8YQ==", + "version": "0.10.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.136.tgz", + "integrity": "sha512-WkvL7BVdoqpNf8QsH4n37Pu7jEZTiJ+OD4FmLMVavw0euhgG18zzJKNKIYRuKcddR52dT/Q8TrspVJofpL98GQ==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -125,58 +125,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.41", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.41.tgz", - "integrity": "sha512-9fgyz86G2JAR2NBcayFse08YoCwQljQCY720raIkGyh3F7iQXs7klNHFUfswg9fLzuO+uQIEh76dxi3NirmCPg==", + "version": "0.2.0-beta.42", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.42.tgz", + "integrity": "sha512-C5w7y6rwSUdRcEiJHFnB2qJI/6DBOi/fJAvTmIpmNZE60cVnrLUuyLmXh6aKbSQ44J6W3PrD5xthb8re3UVUOw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.135.tgz", - "integrity": "sha512-Tk7X0T6AsOcxX9hBjpifHi4iXHrPlvTekroiRnDZEY9S1mdINbUIEphOUwwJxsNuukFsxK1VDIiFsGXsZMNO1Q==", + "version": "0.16.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.136.tgz", + "integrity": "sha512-Y2T/ShQBelmOGy7lup3VEfFF/yXeNkkMXqhGftmjzmwSA+eylFW+92vczMSrckTW++EFvVLR/L5jMXiSw0qOWQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.135.tgz", - "integrity": "sha512-88JtWh9Gm5CRfLtgxCD92JXoz5GyRshhQrythLz83VGI9gZVy9dQnwZxmZ/ZE0G17qzM3Z5yvdfiO70q3cGZHA==", + "version": "0.14.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.136.tgz", + "integrity": "sha512-ursvqITzhZrBQT8XsbOyAQJJKohv33NEm6ToLtMZUmPurBG6KXlVZ9LAPs2YpCBqkifLktSE1GdsofJCpADWuA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.135.tgz", - "integrity": "sha512-IFCCOVVQZ0ZnI7ndy40ZjPGHC64YgSncX9/lUw/py0JXM5bMmeg+VdQvpxsdNlvolkVmU3a26TUbPQeZoi0xzw==", + "version": "0.9.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.136.tgz", + "integrity": "sha512-RwtNbON1uNndrtPCM6qMMElTTpxs7ZLRQVbSm4/BMW6GAt6AbW1RAqwoxMRhbz7VVTux/c3HcKfj3SI1MhqSOw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.135.tgz", - "integrity": "sha512-MuJlcCMlapiVHe1jfk/q2wpciPoW8CCWARjsp38k4hOuGdakyZxtZSUP5iWrIH/OvUh/aHmzIK6Ce6vZ1ObJeg==", + "version": "0.19.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.136.tgz", + "integrity": "sha512-MzVlFKrlgJjKQ6T4/TuamvlvR2FFDvxAPY90lo9u4899k7NNif+M8bBdNea3+bsPMU3fKLhGHoTp0+8MjskaeA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.135" + "@xterm/xterm": "^5.6.0-beta.136" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.135", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.135.tgz", - "integrity": "sha512-LjiC1wH6qPGZyiNLajQbqih5K6FjfHY9WAr+RzGnaN0+u+6kjxNRvq8iQSvXRqkPcSQq/GqC94vZYb6EKyXxag==", + "version": "5.6.0-beta.136", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.136.tgz", + "integrity": "sha512-cOWfdbPUYjV8qJY0yg/HdJBiq/hl8J2NRma563crQbSveDpuiiKV+T+ZVeGKQ2YZztLCz6h+kox6J7LQcPtpiQ==", "license": "MIT" }, "node_modules/commander": { diff --git a/remote/web/package.json b/remote/web/package.json index cff9e6ecb0a..e69c409d24e 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,15 +8,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.2.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.118", - "@xterm/addon-image": "^0.9.0-beta.135", - "@xterm/addon-ligatures": "^0.10.0-beta.135", - "@xterm/addon-progress": "^0.2.0-beta.41", - "@xterm/addon-search": "^0.16.0-beta.135", - "@xterm/addon-serialize": "^0.14.0-beta.135", - "@xterm/addon-unicode11": "^0.9.0-beta.135", - "@xterm/addon-webgl": "^0.19.0-beta.135", - "@xterm/xterm": "^5.6.0-beta.135", + "@xterm/addon-clipboard": "^0.2.0-beta.119", + "@xterm/addon-image": "^0.9.0-beta.136", + "@xterm/addon-ligatures": "^0.10.0-beta.136", + "@xterm/addon-progress": "^0.2.0-beta.42", + "@xterm/addon-search": "^0.16.0-beta.136", + "@xterm/addon-serialize": "^0.14.0-beta.136", + "@xterm/addon-unicode11": "^0.9.0-beta.136", + "@xterm/addon-webgl": "^0.19.0-beta.136", + "@xterm/xterm": "^5.6.0-beta.136", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client-umd": "0.2.0", diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts index 757cb94b83a..10102ebc265 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts @@ -283,8 +283,10 @@ export class PartialTerminalCommand implements ICurrentPartialCommand { constructor( private readonly _xterm: Terminal, + id?: string ) { - this.id = generateUuid(); + //TODO: this does not restore properly due to conflicting with the one created in the. PtyHost + this.id = id ?? generateUuid(); } serialize(cwd: string | undefined): ISerializedTerminalCommand | undefined { diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts index e5f98cdd01f..50f58c6790c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts @@ -312,7 +312,9 @@ function createTerminalCommandElements( element.appendChild(textLabel); disposable.add(dom.addDisposableListener(element, dom.EventType.CLICK, e => { - void clickHandler(); + e.preventDefault(); + e.stopPropagation(); + clickHandler(); })); const hoverElement = dom.$('div.chat-attached-context-hover'); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatTerminalToolProgressPart.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatTerminalToolProgressPart.css index 1eafedcbe42..f7ffbffebbc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatTerminalToolProgressPart.css +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatTerminalToolProgressPart.css @@ -30,6 +30,12 @@ } } +.chat-terminal-content-title.expanded { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + border-bottom: 0; +} + .chat-terminal-content-part .chat-terminal-action-bar { display: flex; gap: 4px; @@ -53,3 +59,41 @@ } } } + +.chat-terminal-output-container.collapsed { display: none; } +.chat-terminal-output-container { + margin-top: 0; + border: 1px solid var(--vscode-chat-requestBorder); + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + background: var(--vscode-panel-background); + max-height: 300px; + font-family: var(--monaco-monospace-font); + box-sizing: border-box; + overflow: hidden; + position: relative; +} +.chat-terminal-output-container.expanded { display: block; } +.chat-terminal-output-container > .monaco-scrollable-element { + width: 100%; +} +.chat-terminal-output-body { + padding: 4px 6px; + box-sizing: border-box; + width: 100%; + height: 100%; +} +.chat-terminal-output-content { + display: flex; + flex-direction: column; + gap: 6px; +} +.chat-terminal-output { + margin: 0; + white-space: pre; + font-size: 12px; +} +.chat-terminal-output-info { + color: var(--vscode-descriptionForeground); + font-size: var(--vscode-chat-font-size-body-xs); +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index ee1ea89b17d..e3c4c84a1f8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -7,7 +7,6 @@ import { h } from '../../../../../../base/browser/dom.js'; import { ActionBar } from '../../../../../../base/browser/ui/actionbar/actionbar.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { isMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js'; -import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IPreferencesService, type IOpenSettingsOptions } from '../../../../../services/preferences/common/preferences.js'; import { migrateLegacyTerminalToolSpecificData } from '../../../common/chat.js'; @@ -23,20 +22,54 @@ import '../media/chatTerminalToolProgressPart.css'; import { TerminalContribSettingId } from '../../../../terminal/terminalContribExports.js'; import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js'; import type { ICodeBlockRenderOptions } from '../../codeBlockPart.js'; -import { ChatConfiguration } from '../../../common/constants.js'; +import { ChatConfiguration, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES } from '../../../common/constants.js'; import { CommandsRegistry } from '../../../../../../platform/commands/common/commands.js'; import { ITerminalChatService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../../terminal/browser/terminal.js'; import { Action, IAction } from '../../../../../../base/common/actions.js'; -import { MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { MutableDisposable, toDisposable, type IDisposable } from '../../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; import { localize } from '../../../../../../nls.js'; import { TerminalLocation } from '../../../../../../platform/terminal/common/terminal.js'; +import { ITerminalCommand, TerminalCapability, type ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import * as domSanitize from '../../../../../../base/browser/domSanitize.js'; +import { DomSanitizerConfig } from '../../../../../../base/browser/domSanitize.js'; +import { allowedMarkdownHtmlAttributes } from '../../../../../../base/browser/markdownRenderer.js'; +import { URI } from '../../../../../../base/common/uri.js'; + +const MAX_TERMINAL_OUTPUT_PREVIEW_HEIGHT = 200; + +const sanitizerConfig = Object.freeze({ + allowedTags: { + augment: ['b', 'i', 'u', 'code', 'span', 'div', 'body', 'pre'], + }, + allowedAttributes: { + augment: [...allowedMarkdownHtmlAttributes, 'style'] + } +}); export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart { public readonly domNode: HTMLElement; private readonly _actionBar = this._register(new MutableDisposable()); + private readonly _outputContainer: HTMLElement; + private readonly _outputBody: HTMLElement; + private readonly _titlePart: HTMLElement; + private _outputScrollbar: DomScrollableElement | undefined; + private _outputContent: HTMLElement | undefined; + private _outputResizeObserver: ResizeObserver | undefined; + + private readonly _showOutputAction = this._register(new MutableDisposable()); + private _showOutputActionAdded = false; + private readonly _focusAction = this._register(new MutableDisposable()); + + private readonly _terminalData: IChatTerminalToolInvocationData; + private _terminalInstance: ITerminalInstance | undefined; + private markdownPart: ChatMarkdownContentPart | undefined; public get codeblocks(): IChatCodeBlockInfo[] { return this.markdownPart?.codeblocks ?? []; @@ -53,19 +86,22 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart codeBlockModelCollection: CodeBlockModelCollection, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, - @ITerminalService private readonly _terminalService: ITerminalService + @ITerminalService private readonly _terminalService: ITerminalService, ) { super(toolInvocation); terminalData = migrateLegacyTerminalToolSpecificData(terminalData); + this._terminalData = terminalData; const elements = h('.chat-terminal-content-part@container', [ h('.chat-terminal-content-title@title'), - h('.chat-terminal-content-message@message') + h('.chat-terminal-content-message@message'), + h('.chat-terminal-output-container@output') ]); const command = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; + this._titlePart = elements.title; const titlePart = this._register(_instantiationService.createInstance( ChatQueryTitlePart, elements.title, @@ -75,11 +111,11 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); // Wait for terminal reconnection to ensure the terminal instance is available - this._terminalService.whenConnected.then(() => { + this._terminalService.whenConnected.then(async () => { // Append the action bar element after the title has been populated so flex order hacks aren't required. const actionBarEl = h('.chat-terminal-action-bar@actionBar'); elements.title.append(actionBarEl.root); - this._createActionBar({ actionBar: actionBarEl.actionBar }, terminalData); + await this._createActionBar({ actionBar: actionBarEl.actionBar }); }); let pastTenseMessage: string | undefined; if (toolInvocation.pastTenseMessage) { @@ -106,42 +142,325 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this._register(this.markdownPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); elements.message.append(this.markdownPart.domNode); + this._outputContainer = elements.output; + this._outputContainer.classList.add('collapsed'); + this._outputBody = dom.$('.chat-terminal-output-body'); + const progressPart = this._register(_instantiationService.createInstance(ChatProgressSubPart, elements.container, this.getIcon(), terminalData.autoApproveInfo)); this.domNode = progressPart.domNode; } - private _createActionBar(elements: { actionBar: HTMLElement }, terminalData: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData): void { + private async _createActionBar(elements: { actionBar: HTMLElement }): Promise { this._actionBar.value = new ActionBar(elements.actionBar, {}); - const terminalToolSessionId = 'terminalToolSessionId' in terminalData ? terminalData.terminalToolSessionId : undefined; - if (!terminalToolSessionId || !elements.actionBar) { + const terminalToolSessionId = this._terminalData.terminalToolSessionId; + if (!terminalToolSessionId) { return; } - const terminalInstance = this._terminalChatService.getTerminalInstanceByToolSessionId(terminalToolSessionId); - if (terminalInstance) { - this._registerInstanceListener(terminalInstance); - this._addFocusAction(terminalInstance, terminalToolSessionId); - } else { - const listener = this._register(this._terminalChatService.onDidRegisterTerminalInstanceWithToolSession(terminalInstance => { - this._registerInstanceListener(terminalInstance); - this._addFocusAction(terminalInstance, terminalToolSessionId); - this._store.delete(listener); - })); - } + + const attachInstance = async (instance: ITerminalInstance | undefined) => { + if (!instance || this._terminalInstance === instance) { + return; + } + this._terminalInstance = instance; + this._registerInstanceListener(instance); + await this._addFocusAction(instance, terminalToolSessionId); + if (this._terminalData?.output?.html) { + this._ensureShowOutputAction(); + } + }; + + await attachInstance(await this._terminalChatService.getTerminalInstanceByToolSessionId(terminalToolSessionId)); + + const listener = this._terminalChatService.onDidRegisterTerminalInstanceWithToolSession(async instance => { + if (instance !== await this._terminalChatService.getTerminalInstanceByToolSessionId(terminalToolSessionId)) { + return; + } + attachInstance(instance); + listener.dispose(); + }); + this._register(listener); } - private _addFocusAction(terminalInstance: ITerminalInstance, terminalToolSessionId: string) { + private async _addFocusAction(terminalInstance: ITerminalInstance, terminalToolSessionId: string) { + if (!this._actionBar.value) { + return; + } const isTerminalHidden = this._terminalChatService.isBackgroundTerminal(terminalToolSessionId); - const focusAction = this._register(this._instantiationService.createInstance(FocusChatInstanceAction, terminalInstance, isTerminalHidden)); - this._actionBar.value?.push([focusAction], { icon: true, label: false }); + const command = this._getResolvedCommand(terminalInstance); + const focusAction = this._instantiationService.createInstance(FocusChatInstanceAction, terminalInstance, command, isTerminalHidden); + this._focusAction.value = focusAction; + this._actionBar.value.push(focusAction, { icon: true, label: false, index: 0 }); + this._ensureShowOutputAction(); + } + + private _ensureShowOutputAction(): void { + if (!this._actionBar.value) { + return; + } + const hasSerializedOutput = !!this._terminalData.output?.html; + const commandFinished = !!this._getResolvedCommand()?.endMarker; + if (!hasSerializedOutput && !commandFinished) { + return; + } + let showOutputAction = this._showOutputAction.value; + if (!showOutputAction) { + showOutputAction = new ToggleChatTerminalOutputAction(expanded => this._toggleOutput(expanded)); + this._showOutputAction.value = showOutputAction; + } + showOutputAction.syncPresentation(this._outputContainer.classList.contains('expanded')); + + const actionBar = this._actionBar.value; + if (this._showOutputActionAdded) { + const existingIndex = actionBar.viewItems.findIndex(item => item.action === showOutputAction); + if (existingIndex >= 0 && existingIndex !== actionBar.length() - 1) { + actionBar.pull(existingIndex); + this._showOutputActionAdded = false; + } else if (existingIndex >= 0) { + return; + } + } + + if (this._showOutputActionAdded) { + return; + } + actionBar.push([showOutputAction], { icon: true, label: false }); + this._showOutputActionAdded = true; + } + + private _getResolvedCommand(instance?: ITerminalInstance): ITerminalCommand | undefined { + const target = instance ?? this._terminalInstance; + if (!target) { + return undefined; + } + return this._resolveCommand(target); } private _registerInstanceListener(terminalInstance: ITerminalInstance) { + const commandDetectionListener = this._register(new MutableDisposable()); + const tryResolveCommand = (): ITerminalCommand | undefined => { + const resolvedCommand = this._resolveCommand(terminalInstance); + if (resolvedCommand?.endMarker) { + this._ensureShowOutputAction(); + } + return resolvedCommand; + }; + + const attachCommandDetection = (commandDetection: ICommandDetectionCapability | undefined) => { + commandDetectionListener.clear(); + if (!commandDetection) { + return; + } + + const resolvedImmediately = tryResolveCommand(); + if (resolvedImmediately?.endMarker) { + return; + } + + commandDetectionListener.value = commandDetection.onCommandFinished(() => { + this._ensureShowOutputAction(); + commandDetectionListener.clear(); + }); + }; + + attachCommandDetection(terminalInstance.capabilities.get(TerminalCapability.CommandDetection)); + this._register(terminalInstance.capabilities.onDidAddCommandDetectionCapability(cd => attachCommandDetection(cd))); + const instanceListener = this._register(terminalInstance.onDisposed(() => { + if (this._terminalInstance === terminalInstance) { + this._terminalInstance = undefined; + } + commandDetectionListener.clear(); this._actionBar.clear(); - instanceListener?.dispose(); + this._focusAction.clear(); + const keepOutputAction = !!this._terminalData.output?.html; + this._showOutputActionAdded = false; + if (!keepOutputAction) { + this._showOutputAction.clear(); + } + this._ensureShowOutputAction(); + instanceListener.dispose(); })); } + + private async _toggleOutput(expanded: boolean): Promise { + const currentlyExpanded = this._outputContainer.classList.contains('expanded'); + if (expanded === currentlyExpanded) { + this._showOutputAction.value?.syncPresentation(currentlyExpanded); + return false; + } + + this._setOutputExpanded(expanded); + + if (!expanded) { + this._layoutOutput(); + this._showOutputAction.value?.syncPresentation(false); + return true; + } + + const didCreate = await this._renderOutputIfNeeded(); + this._layoutOutput(); + this._scrollOutputToBottom(); + if (didCreate) { + this._scheduleOutputRelayout(); + } + this._showOutputAction.value?.syncPresentation(expanded); + return true; + } + + private _setOutputExpanded(expanded: boolean): void { + this._outputContainer.classList.toggle('expanded', expanded); + this._outputContainer.classList.toggle('collapsed', !expanded); + this._titlePart.classList.toggle('expanded', expanded); + } + + private async _renderOutputIfNeeded(): Promise { + if (this._outputContent) { + this._ensureOutputResizeObserver(); + return false; + } + + if (!this._terminalInstance) { + const resource = this._getTerminalResource(); + if (resource) { + this._terminalInstance = this._terminalService.getInstanceFromResource(resource); + } + } + const output = await this._collectOutput(this._terminalInstance); + const content = this._renderOutput(output); + const theme = this._terminalInstance?.xterm?.getXtermTheme(); + if (theme) { + const inlineTerminal = content.querySelector('div'); + if (inlineTerminal) { + inlineTerminal.style.setProperty('background-color', theme.background || 'transparent'); + inlineTerminal.style.setProperty('color', theme.foreground || 'inherit'); + } + } + + this._outputBody.replaceChildren(content); + this._outputContent = content; + if (!this._outputScrollbar) { + this._outputScrollbar = this._register(new DomScrollableElement(this._outputBody, { + vertical: ScrollbarVisibility.Auto, + horizontal: ScrollbarVisibility.Auto, + handleMouseWheel: true + })); + const scrollableDomNode = this._outputScrollbar.getDomNode(); + scrollableDomNode.tabIndex = 0; + scrollableDomNode.style.maxHeight = `${MAX_TERMINAL_OUTPUT_PREVIEW_HEIGHT}px`; + this._outputContainer.appendChild(scrollableDomNode); + this._ensureOutputResizeObserver(); + this._outputContent = undefined; + } else { + this._ensureOutputResizeObserver(); + } + + return true; + } + + private _scrollOutputToBottom(): void { + if (!this._outputScrollbar) { + return; + } + const dimensions = this._outputScrollbar.getScrollDimensions(); + this._outputScrollbar.setScrollPosition({ scrollTop: dimensions.scrollHeight }); + } + + private _scheduleOutputRelayout(): void { + dom.getActiveWindow().requestAnimationFrame(() => { + this._layoutOutput(); + this._scrollOutputToBottom(); + }); + } + + private _layoutOutput(): void { + if (!this._outputScrollbar || !this._outputContainer.classList.contains('expanded')) { + return; + } + const scrollableDomNode = this._outputScrollbar.getDomNode(); + const viewportHeight = Math.min(this._outputBody.scrollHeight, MAX_TERMINAL_OUTPUT_PREVIEW_HEIGHT); + scrollableDomNode.style.height = `${viewportHeight}px`; + this._outputScrollbar.scanDomNode(); + } + + private _ensureOutputResizeObserver(): void { + if (this._outputResizeObserver || !this._outputScrollbar) { + return; + } + const observer = new ResizeObserver(() => this._layoutOutput()); + observer.observe(this._outputContainer); + this._outputResizeObserver = observer; + this._register(toDisposable(() => { + observer.disconnect(); + this._outputResizeObserver = undefined; + })); + } + + private async _collectOutput(terminalInstance: ITerminalInstance | undefined): Promise<{ text: string; truncated: boolean }> { + const storedOutput = this._terminalData.output; + if (storedOutput?.html) { + return { text: storedOutput.html, truncated: storedOutput.truncated ?? false }; + } + if (!terminalInstance) { + return { text: '', truncated: false }; + } + const xterm = await terminalInstance.xtermReadyPromise; + if (!xterm) { + return { text: '', truncated: false }; + } + const command = this._resolveCommand(terminalInstance); + if (!command?.endMarker) { + return { text: '', truncated: false }; + } + const text = await xterm.getCommandOutputAsHtml(command, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES); + if (!text) { + return { text: '', truncated: false }; + } + + return { text, truncated: false }; + } + + private _renderOutput(result: { text: string; truncated: boolean }): HTMLElement { + const container = document.createElement('div'); + container.classList.add('chat-terminal-output-content'); + + const pre = document.createElement('pre'); + pre.classList.add('chat-terminal-output'); + domSanitize.safeSetInnerHtml(pre, result.text, sanitizerConfig); + container.appendChild(pre); + + if (result.truncated) { + const note = document.createElement('div'); + note.classList.add('chat-terminal-output-info'); + note.textContent = localize('chat.terminalOutputTruncated', 'Output truncated to first {0} characters.', CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES); + container.appendChild(note); + } + + return container; + } + + private _getTerminalResource(): URI | undefined { + const commandUri = this._terminalData.terminalCommandUri; + if (!commandUri) { + return undefined; + } + return URI.isUri(commandUri) ? commandUri : URI.revive(commandUri); + } + + + private _resolveCommand(instance: ITerminalInstance): ITerminalCommand | undefined { + const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection); + const commands = commandDetection?.commands; + if (!commands || commands.length === 0) { + return undefined; + } + + const commandId = this._terminalChatService.getTerminalCommandIdByToolSessionId(this._terminalData.terminalToolSessionId); + if (commandId) { + return commands.find(c => c.id === commandId); + } + return; + } } export const openTerminalSettingsLinkCommandId = '_chat.openTerminalSettingsLink'; @@ -181,9 +500,43 @@ CommandsRegistry.registerCommand(openTerminalSettingsLinkCommandId, async (acces } }); +class ToggleChatTerminalOutputAction extends Action implements IAction { + private _expanded = false; + + constructor(private readonly _toggle: (expanded: boolean) => Promise) { + super( + 'chat.showTerminalOutput', + localize('showTerminalOutput', 'Show Output'), + ThemeIcon.asClassName(Codicon.chevronRight), + true, + ); + } + + public override async run(): Promise { + const target = !this._expanded; + await this._toggle(target); + } + + public syncPresentation(expanded: boolean): void { + this._expanded = expanded; + this._updatePresentation(); + } + + private _updatePresentation(): void { + if (this._expanded) { + this.label = localize('hideTerminalOutput', 'Hide Output'); + this.class = ThemeIcon.asClassName(Codicon.chevronDown); + } else { + this.label = localize('showTerminalOutput', 'Show Output'); + this.class = ThemeIcon.asClassName(Codicon.chevronRight); + } + } +} + export class FocusChatInstanceAction extends Action implements IAction { constructor( private readonly _instance: ITerminalInstance, + private readonly _command: ITerminalCommand | undefined, isTerminalHidden: boolean, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, @@ -199,12 +552,16 @@ export class FocusChatInstanceAction extends Action implements IAction { public override async run() { this.label = localize('focusTerminal', 'Focus Terminal'); + this._terminalService.setActiveInstance(this._instance); if (this._instance.target === TerminalLocation.Editor) { this._terminalEditorService.openEditor(this._instance); } else { - this._terminalGroupService.showPanel(true); + await this._terminalGroupService.showPanel(true); } this._terminalService.setActiveInstance(this._instance); await this._instance?.focusWhenReady(true); + if (this._command) { + this._instance.xterm?.markTracker.revealCommand(this._command); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 828972f3e7c..56f43e39750 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -2305,6 +2305,16 @@ have to be updated for changes to the rules above, or to support more deeply nes overflow: auto; } +.chat-terminal-output-container { + max-height: 100%; + max-width: 100%; + margin-right: 20px; +} + +.chat-terminal-content-title.expanded { + margin-right: 20px; +} + .chat-attached-context-attachment .chat-attached-context-pill { font-size: 12px; display: inline-flex; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index c5cecf4ad78..1037d3ba259 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -313,7 +313,12 @@ export interface IChatTerminalToolInvocationData { alternativeRecommendation?: string; language: string; terminalToolSessionId?: string; + terminalCommandUri?: UriComponents; autoApproveInfo?: IMarkdownString; + output?: { + html: string; + truncated?: boolean; + }; } /** diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 61f16849f71..074eaa63c1b 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -121,3 +121,5 @@ export function isSupportedChatFileScheme(accessor: ServicesAccessor, scheme: st export const AGENT_SESSIONS_VIEWLET_ID = 'workbench.view.chat.sessions'; // TODO@bpasero clear once settled export const MANAGE_CHAT_COMMAND_ID = 'workbench.action.chat.manage'; export const ChatEditorTitleMaxLength = 30; + +export const CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES = 1000; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 090b8aaa839..a43080077ff 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -9,7 +9,7 @@ import { Color } from '../../../../base/common/color.js'; import { Event, IDynamicListEventMultiplexer, type DynamicListEventMultiplexer } from '../../../../base/common/event.js'; import { DisposableStore, IDisposable, type IReference } from '../../../../base/common/lifecycle.js'; import { OperatingSystem } from '../../../../base/common/platform.js'; -import { URI } from '../../../../base/common/uri.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeyMods } from '../../../../platform/quickinput/common/quickInput.js'; import { IMarkProperties, ITerminalCapabilityImplMap, ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; @@ -124,7 +124,12 @@ export interface ITerminalChatService { * @param terminalToolSessionId The tool session id provided in toolSpecificData. * If no tool session ID is provided, we do nothing. */ - getTerminalInstanceByToolSessionId(terminalToolSessionId: string): ITerminalInstance | undefined; + getTerminalInstanceByToolSessionId(terminalToolSessionId: string): Promise; + + /** + * Get the terminal command ID associated with a tool session ID, if any. + */ + getTerminalCommandIdByToolSessionId(terminalToolSessionId: string | undefined): string | undefined; /** * Returns the list of terminal instances that have been registered with a tool session id. @@ -650,7 +655,7 @@ export interface ITerminalInstanceHost { * Gets an instance from a resource if it exists. This MUST be used instead of getInstanceFromId * when you only know about a terminal's URI. (a URI's instance ID may not be this window's instance ID) */ - getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined; + getInstanceFromResource(resource: UriComponents | undefined): ITerminalInstance | undefined; } /** diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index af09495cfe6..85375c996d4 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -368,6 +368,65 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach return this._serializeAddon.serializeAsHTML(); } + async getCommandOutputAsHtml(command: ITerminalCommand, maxLines: number): Promise { + if (!this._serializeAddon) { + const Addon = await this._xtermAddonLoader.importAddon('serialize'); + this._serializeAddon = new Addon(); + this.raw.loadAddon(this._serializeAddon); + } + let startLine: number; + let startCol: number; + if (command.executedMarker && command.executedMarker.line >= 0) { + startLine = command.executedMarker.line; + startCol = Math.max(command.executedX ?? 0, 0); + } else { + startLine = command.marker?.line !== undefined ? command.marker.line + 1 : 1; + startCol = Math.max(command.startX ?? 0, 0); + } + + let endLine = command.endMarker?.line !== undefined ? command.endMarker.line - 1 : this.raw.buffer.active.length - 1; + if (endLine < startLine) { + return ''; + } + // Trim empty lines from the end + let emptyLinesFromEnd = 0; + for (let i = endLine; i >= startLine; i--) { + const line = this.raw.buffer.active.getLine(i); + if (line && line.translateToString(true).trim() === '') { + emptyLinesFromEnd++; + } else { + break; + } + } + endLine = endLine - emptyLinesFromEnd; + + // Trim empty lines from the start + let emptyLinesFromStart = 0; + for (let i = startLine; i <= endLine; i++) { + const line = this.raw.buffer.active.getLine(i); + if (line && line.translateToString(true).trim() === '') { + emptyLinesFromStart++; + } else { + break; + } + } + startLine = startLine + emptyLinesFromStart; + + if (maxLines && endLine - startLine > maxLines) { + startLine = endLine - maxLines; + startCol = 0; + } + + const bufferLine = this.raw.buffer.active.getLine(startLine); + if (bufferLine) { + startCol = Math.min(startCol, bufferLine.length); + } + + const range = { startLine, endLine, startCol }; + const result = this._serializeAddon.serializeAsHTML({ range }); + return result; + } + async getSelectionAsHtml(command?: ITerminalCommand): Promise { if (!this._serializeAddon) { const Addon = await this._xtermAddonLoader.importAddon('serialize'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts index c52bc85339d..438bac8d1bc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts @@ -9,17 +9,24 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { ITerminalChatService, ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { IChatService } from '../../../chat/common/chatService.js'; import { TerminalChatContextKeys } from './terminalChat.js'; +const enum StorageKeys { + ToolSessionMappings = 'terminalChat.toolSessionMappings', + CommandIdMappings = 'terminalChat.commandIdMappings' +} + + /** * Used to manage chat tool invocations and the underlying terminal instances they create/use. */ export class TerminalChatService extends Disposable implements ITerminalChatService { declare _serviceBrand: undefined; - private static readonly _storageKey = 'terminalChat.toolSessionMappings'; - private readonly _terminalInstancesByToolSessionId = new Map(); + private readonly _commandIdByToolSessionId = new Map(); private readonly _terminalInstanceListenersByToolSessionId = this._register(new DisposableMap()); private readonly _onDidRegisterTerminalInstanceForToolSession = new Emitter(); readonly onDidRegisterTerminalInstanceWithToolSession: Event = this._onDidRegisterTerminalInstanceForToolSession.event; @@ -37,7 +44,8 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ @ILogService private readonly _logService: ILogService, @ITerminalService private readonly _terminalService: ITerminalService, @IStorageService private readonly _storageService: IStorageService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IChatService private readonly _chatService: IChatService, ) { super(); @@ -59,6 +67,20 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._persistToStorage(); this._updateHasToolTerminalContextKey(); })); + const listener = this._register(instance.capabilities.get(TerminalCapability.CommandDetection)!.onCommandFinished(e => { + this._commandIdByToolSessionId.set(terminalToolSessionId, e.id); + this._persistToStorage(); + listener.dispose(); + })); + this._register(this._chatService.onDidDisposeSession(e => { + if (e.sessionId === terminalToolSessionId) { + this._terminalInstancesByToolSessionId.delete(terminalToolSessionId); + this._terminalInstanceListenersByToolSessionId.deleteAndDispose(terminalToolSessionId); + this._commandIdByToolSessionId.delete(terminalToolSessionId); + this._persistToStorage(); + this._updateHasToolTerminalContextKey(); + } + })); if (typeof instance.persistentProcessId === 'number') { this._persistToStorage(); @@ -67,7 +89,18 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._updateHasToolTerminalContextKey(); } - getTerminalInstanceByToolSessionId(terminalToolSessionId: string | undefined): ITerminalInstance | undefined { + getTerminalCommandIdByToolSessionId(terminalToolSessionId: string | undefined): string | undefined { + if (!terminalToolSessionId) { + return undefined; + } + if (this._commandIdByToolSessionId.size === 0) { + this._restoreFromStorage(); + } + return this._commandIdByToolSessionId.get(terminalToolSessionId); + } + + async getTerminalInstanceByToolSessionId(terminalToolSessionId: string | undefined): Promise { + await this._terminalService.whenConnected; if (!terminalToolSessionId) { return undefined; } @@ -99,7 +132,7 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ private _restoreFromStorage(): void { try { - const raw = this._storageService.get(TerminalChatService._storageKey, StorageScope.WORKSPACE); + const raw = this._storageService.get(StorageKeys.ToolSessionMappings, StorageScope.WORKSPACE); if (!raw) { return; } @@ -109,6 +142,15 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._pendingRestoredMappings.set(toolSessionId, persistentProcessId); } } + const rawCommandIds = this._storageService.get(StorageKeys.CommandIdMappings, StorageScope.WORKSPACE); + if (rawCommandIds) { + const parsedCommandIds: [string, string][] = JSON.parse(rawCommandIds); + for (const [toolSessionId, commandId] of parsedCommandIds) { + if (typeof toolSessionId === 'string' && typeof commandId === 'string') { + this._commandIdByToolSessionId.set(toolSessionId, commandId); + } + } + } } catch (err) { this._logService.warn('Failed to restore terminal chat tool session mappings', err); } @@ -147,9 +189,18 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ } } if (entries.length > 0) { - this._storageService.store(TerminalChatService._storageKey, JSON.stringify(entries), StorageScope.WORKSPACE, StorageTarget.MACHINE); + this._storageService.store(StorageKeys.ToolSessionMappings, JSON.stringify(entries), StorageScope.WORKSPACE, StorageTarget.MACHINE); } else { - this._storageService.remove(TerminalChatService._storageKey, StorageScope.WORKSPACE); + this._storageService.remove(StorageKeys.ToolSessionMappings, StorageScope.WORKSPACE); + } + const commandEntries: [string, string][] = []; + for (const [toolSessionId, commandId] of this._commandIdByToolSessionId.entries()) { + commandEntries.push([toolSessionId, commandId]); + } + if (commandEntries.length > 0) { + this._storageService.store(StorageKeys.CommandIdMappings, JSON.stringify(commandEntries), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } else { + this._storageService.remove(StorageKeys.CommandIdMappings, StorageScope.WORKSPACE); } } catch (err) { this._logService.warn('Failed to persist terminal chat tool session mappings', err); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 0dd4a1779f6..233c5a4c62b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -13,18 +13,18 @@ import { MarkdownString, type IMarkdownString } from '../../../../../../base/com import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { basename } from '../../../../../../base/common/path.js'; import { OperatingSystem, OS } from '../../../../../../base/common/platform.js'; -import { count } from '../../../../../../base/common/strings.js'; -import type { DeepImmutable } from '../../../../../../base/common/types.js'; +import { count, escape } from '../../../../../../base/common/strings.js'; import { generateUuid } from '../../../../../../base/common/uuid.js'; import { localize } from '../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService, type ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { TerminalCapability, type ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js'; import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js'; import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService.js'; +import { CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES } from '../../../../chat/common/constants.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/languageModelToolsService.js'; import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import type { XtermTerminal } from '../../../../terminal/browser/xterm/xtermTerminal.js'; @@ -37,7 +37,7 @@ import type { ITerminalExecuteStrategy } from '../executeStrategy/executeStrateg import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import { isFish, isPowerShell, isWindowsPowerShell, isZsh, sanitizeTerminalOutput } from '../runInTerminalHelpers.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; @@ -403,8 +403,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { - - const toolSpecificData = invocation.toolSpecificData as DeepImmutable | undefined; + const toolSpecificData = invocation.toolSpecificData as IChatTerminalToolInvocationData | undefined; if (!toolSpecificData) { throw new Error('toolSpecificData must be provided for this tool'); } @@ -460,6 +459,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { throw new Error('Instance was disposed before xterm.js was ready'); } + const commandDetection = toolTerminal.instance.capabilities.get(TerminalCapability.CommandDetection); + let inputUserChars = 0; let inputUserSigint = false; store.add(xterm.raw.onData(data => { @@ -480,6 +481,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, execution, undefined, invocation.context!, token, command)); await Event.toPromise(outputMonitor.onDidFinishCommand); const pollingResult = outputMonitor.pollingResult; + await this._updateTerminalCommandMetadata(toolSpecificData, toolTerminal.instance, commandDetection, pollingResult?.output ? { text: pollingResult.output } : undefined); if (token.isCancellationRequested) { throw new CancellationError(); @@ -515,6 +517,12 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { error = e instanceof CancellationError ? 'canceled' : 'unexpectedException'; throw e; } finally { + await this._updateTerminalCommandMetadata( + toolSpecificData, + toolTerminal.instance, + commandDetection, + pollingResult?.output ? { text: pollingResult.output } : undefined + ); store.dispose(); this._logService.debug(`RunInTerminalTool: Finished polling \`${pollingResult?.output.length}\` lines of output in \`${pollingResult?.pollDurationMs}\``); const timingExecuteMs = Date.now() - timingStart; @@ -551,7 +559,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let exitCode: number | undefined; try { let strategy: ITerminalExecuteStrategy; - const commandDetection = toolTerminal.instance.capabilities.get(TerminalCapability.CommandDetection); switch (toolTerminal.shellIntegrationQuality) { case ShellIntegrationQuality.None: { strategy = this._instantiationService.createInstance(NoneExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false); @@ -600,6 +607,12 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { error = e instanceof CancellationError ? 'canceled' : 'unexpectedException'; throw e; } finally { + await this._updateTerminalCommandMetadata( + toolSpecificData, + toolTerminal.instance, + commandDetection, + terminalResult ? { text: terminalResult } : undefined + ); store.dispose(); const timingExecuteMs = Date.now() - timingStart; this._telemetry.logInvoke(toolTerminal.instance, { @@ -650,6 +663,58 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } + private async _updateTerminalCommandMetadata( + toolSpecificData: IChatTerminalToolInvocationData, + instance: ITerminalInstance, + commandDetection: ICommandDetectionCapability | undefined, + fallbackOutput?: { text: string; truncated?: boolean } + ): Promise { + const command = commandDetection?.commands.at(-1); + if (command?.id && !toolSpecificData.terminalCommandUri) { + const params = new URLSearchParams(instance.resource.query); + params.set('command', command.id); + const commandUri = instance.resource.with({ query: params.toString() || undefined }); + toolSpecificData.terminalCommandUri = commandUri; + } + + if (toolSpecificData.output?.html) { + return; + } + + let serializedHtml: string | undefined; + let truncated = fallbackOutput?.truncated ?? false; + + if (command?.endMarker) { + try { + const xterm = await instance.xtermReadyPromise; + if (xterm) { + const html = await xterm.getCommandOutputAsHtml(command, CHAT_TERMINAL_OUTPUT_MAX_PREVIEW_LINES); + if (html) { + serializedHtml = html; + truncated = false; + } + } + } catch (error) { + this._logService.debug('RunInTerminalTool: Failed to capture terminal HTML output for serialization', error); + } + } + + if (!serializedHtml && fallbackOutput?.text) { + const sanitized = sanitizeTerminalOutput(fallbackOutput.text); + if (sanitized) { + serializedHtml = escape(sanitized); + truncated = fallbackOutput.truncated ?? truncated; + } + } + + if (serializedHtml) { + toolSpecificData.output = { + html: serializedHtml, + truncated + }; + } + } + private _handleTerminalVisibility(toolTerminal: IToolTerminal) { if (this._configurationService.getValue(TerminalChatAgentToolsSettingId.OutputLocation) === 'terminal') { this._terminalService.setActiveInstance(toolTerminal.instance);