mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-10 19:37:17 -07:00
Right click to paste in Alternate Buffer mode (#13234)
This commit is contained in:
committed by
GitHub
parent
55438dd687
commit
2171ac1144
@@ -87,7 +87,7 @@ available combinations.
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ---------- |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste an image from the clipboard. | `Ctrl + V` |
|
||||
| Paste from the clipboard. | `Ctrl + V` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
|
||||
Generated
+84
-15
@@ -2301,6 +2301,7 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2481,6 +2482,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2514,6 +2516,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2882,6 +2885,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2915,6 +2919,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1"
|
||||
@@ -2967,6 +2972,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1",
|
||||
@@ -4148,6 +4154,7 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4435,6 +4442,7 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -5202,6 +5210,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5556,8 +5565,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-includes": {
|
||||
"version": "3.1.9",
|
||||
@@ -6510,6 +6518,24 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/clipboardy": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-5.0.0.tgz",
|
||||
"integrity": "sha512-MQfKHaD09eP80Pev4qBxZLbxJK/ONnqfSYAPlCmPh+7BDboYtO/3BmB6HGzxDIT0SlTRc2tzS8lQqfcdLtZ0Kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"execa": "^9.6.0",
|
||||
"is-wayland": "^0.1.0",
|
||||
"is-wsl": "^3.1.0",
|
||||
"is64bit": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -6803,7 +6829,6 @@
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
@@ -7810,6 +7835,7 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8399,7 +8425,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -8409,7 +8434,6 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -8419,7 +8443,6 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8649,7 +8672,6 @@
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
@@ -8668,7 +8690,6 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -8677,15 +8698,13 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -9890,6 +9909,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.3.tgz",
|
||||
"integrity": "sha512-2qm05tjtdia+d1gD7LQjPJyCPJluKDuR5B+FI3ZZXshFoU1igZBFvXs2++x9OT6d9755q+gkRPOdtH8jzx5MiQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -10535,6 +10555,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wayland": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wayland/-/is-wayland-0.1.0.tgz",
|
||||
"integrity": "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-weakmap": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
||||
@@ -10596,6 +10628,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is64bit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz",
|
||||
"integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"system-architecture": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
@@ -12941,8 +12988,7 @@
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "3.0.0",
|
||||
@@ -13476,6 +13522,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13486,6 +13533,7 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -15172,6 +15220,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/system-architecture": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz",
|
||||
"integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/table": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
|
||||
@@ -15546,6 +15606,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15710,7 +15771,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -15718,6 +15780,7 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -15902,6 +15965,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16063,7 +16127,6 @@
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
@@ -16119,6 +16182,7 @@
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16235,6 +16299,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16248,6 +16313,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -16945,6 +17011,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -17258,6 +17325,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"ansi-regex": "^6.2.2",
|
||||
"clipboardy": "^5.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"comment-json": "^4.2.5",
|
||||
"diff": "^7.0.0",
|
||||
@@ -17485,6 +17553,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"ansi-regex": "^6.2.2",
|
||||
"clipboardy": "^5.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"comment-json": "^4.2.5",
|
||||
"diff": "^7.0.0",
|
||||
|
||||
@@ -54,7 +54,7 @@ export enum Command {
|
||||
|
||||
// External tools
|
||||
OPEN_EXTERNAL_EDITOR = 'openExternalEditor',
|
||||
PASTE_CLIPBOARD_IMAGE = 'pasteClipboardImage',
|
||||
PASTE_CLIPBOARD = 'pasteClipboard',
|
||||
|
||||
// App level bindings
|
||||
SHOW_ERROR_DETAILS = 'showErrorDetails',
|
||||
@@ -192,7 +192,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
{ key: 'x', ctrl: true },
|
||||
{ sequence: '\x18', ctrl: true },
|
||||
],
|
||||
[Command.PASTE_CLIPBOARD_IMAGE]: [{ key: 'v', ctrl: true }],
|
||||
[Command.PASTE_CLIPBOARD]: [{ key: 'v', ctrl: true }],
|
||||
|
||||
// App level bindings
|
||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }],
|
||||
@@ -292,7 +292,7 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
},
|
||||
{
|
||||
title: 'External Tools',
|
||||
commands: [Command.OPEN_EXTERNAL_EDITOR, Command.PASTE_CLIPBOARD_IMAGE],
|
||||
commands: [Command.OPEN_EXTERNAL_EDITOR, Command.PASTE_CLIPBOARD],
|
||||
},
|
||||
{
|
||||
title: 'App Controls',
|
||||
@@ -344,7 +344,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.NEWLINE]: 'Insert a newline without submitting.',
|
||||
[Command.OPEN_EXTERNAL_EDITOR]:
|
||||
'Open the current prompt in an external editor.',
|
||||
[Command.PASTE_CLIPBOARD_IMAGE]: 'Paste an image from the clipboard.',
|
||||
[Command.PASTE_CLIPBOARD]: 'Paste from the clipboard.',
|
||||
[Command.SHOW_ERROR_DETAILS]: 'Toggle detailed error information.',
|
||||
[Command.SHOW_FULL_TODOS]: 'Toggle the full TODO list.',
|
||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: 'Toggle IDE context details.',
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { UseInputHistoryReturn } from '../hooks/useInputHistory.js';
|
||||
import { useInputHistory } from '../hooks/useInputHistory.js';
|
||||
import type { UseReverseSearchCompletionReturn } from '../hooks/useReverseSearchCompletion.js';
|
||||
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
||||
import clipboardy from 'clipboardy';
|
||||
import * as clipboardUtils from '../utils/clipboardUtils.js';
|
||||
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
@@ -35,6 +36,7 @@ vi.mock('../hooks/useShellHistory.js');
|
||||
vi.mock('../hooks/useCommandCompletion.js');
|
||||
vi.mock('../hooks/useInputHistory.js');
|
||||
vi.mock('../hooks/useReverseSearchCompletion.js');
|
||||
vi.mock('clipboardy');
|
||||
vi.mock('../utils/clipboardUtils.js');
|
||||
vi.mock('../hooks/useKittyKeyboardProtocol.js');
|
||||
|
||||
@@ -146,6 +148,7 @@ describe('InputPrompt', () => {
|
||||
deleteWordLeft: vi.fn(),
|
||||
deleteWordRight: vi.fn(),
|
||||
visualToLogicalMap: [[0, 0]],
|
||||
getOffset: vi.fn().mockReturnValue(0),
|
||||
} as unknown as TextBuffer;
|
||||
|
||||
mockShellHistory = {
|
||||
@@ -505,6 +508,7 @@ describe('InputPrompt', () => {
|
||||
// Set initial text and cursor position
|
||||
mockBuffer.text = 'Hello world';
|
||||
mockBuffer.cursor = [0, 5]; // Cursor after "Hello"
|
||||
vi.mocked(mockBuffer.getOffset).mockReturnValue(5);
|
||||
mockBuffer.lines = ['Hello world'];
|
||||
mockBuffer.replaceRangeByOffset = vi.fn();
|
||||
|
||||
@@ -559,6 +563,32 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('clipboard text paste', () => {
|
||||
it('should insert text from clipboard on Ctrl+V', async () => {
|
||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
|
||||
vi.mocked(clipboardy.read).mockResolvedValue('pasted text');
|
||||
vi.mocked(mockBuffer.replaceRangeByOffset).mockClear();
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x16'); // Ctrl+V
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clipboardy.read).toHaveBeenCalled();
|
||||
expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
expect.any(Number),
|
||||
'pasted text',
|
||||
);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'should complete a partial parent command',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import clipboardy from 'clipboardy';
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { Box, Text, getBoundingBox, type DOMElement } from 'ink';
|
||||
import { SuggestionsDisplay, MAX_WIDTH } from './SuggestionsDisplay.js';
|
||||
@@ -315,7 +316,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}, [buffer, popAllMessages, inputHistory]);
|
||||
|
||||
// Handle clipboard image pasting with Ctrl+V
|
||||
const handleClipboardImage = useCallback(async () => {
|
||||
const handleClipboardPaste = useCallback(async () => {
|
||||
try {
|
||||
if (await clipboardHasImage()) {
|
||||
const imagePath = await saveClipboardImage(config.getTargetDir());
|
||||
@@ -331,14 +332,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
// Insert @path reference at cursor position
|
||||
const insertText = `@${relativePath}`;
|
||||
const currentText = buffer.text;
|
||||
const [row, col] = buffer.cursor;
|
||||
|
||||
// Calculate offset from row/col
|
||||
let offset = 0;
|
||||
for (let i = 0; i < row; i++) {
|
||||
offset += buffer.lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
offset += col;
|
||||
const offset = buffer.getOffset();
|
||||
|
||||
// Add spaces around the path if needed
|
||||
let textToInsert = insertText;
|
||||
@@ -355,8 +349,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
// Insert at cursor position
|
||||
buffer.replaceRangeByOffset(offset, offset, textToInsert);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const textToInsert = await clipboardy.read();
|
||||
const offset = buffer.getOffset();
|
||||
buffer.replaceRangeByOffset(offset, offset, textToInsert);
|
||||
} catch (error) {
|
||||
console.error('Error handling clipboard image:', error);
|
||||
}
|
||||
@@ -381,10 +380,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
buffer.moveToVisualPosition(visualRow, relX);
|
||||
return true;
|
||||
}
|
||||
} else if (event.name === 'right-release') {
|
||||
handleClipboardPaste();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[buffer],
|
||||
[buffer, handleClipboardPaste],
|
||||
);
|
||||
|
||||
useMouse(handleMouse, { isActive: focus && !isEmbeddedShellFocused });
|
||||
@@ -772,9 +773,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+V for clipboard image paste
|
||||
if (keyMatchers[Command.PASTE_CLIPBOARD_IMAGE](key)) {
|
||||
handleClipboardImage();
|
||||
// Ctrl+V for clipboard paste
|
||||
if (keyMatchers[Command.PASTE_CLIPBOARD](key)) {
|
||||
handleClipboardPaste();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -805,7 +806,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
handleSubmit,
|
||||
shellHistory,
|
||||
reverseSearchCompletion,
|
||||
handleClipboardImage,
|
||||
handleClipboardPaste,
|
||||
resetCompletionState,
|
||||
showEscapePrompt,
|
||||
resetEscapeState,
|
||||
|
||||
@@ -2040,6 +2040,11 @@ export function useTextBuffer({
|
||||
[visualLayout],
|
||||
);
|
||||
|
||||
const getOffset = useCallback(
|
||||
(): number => logicalPosToOffset(lines, cursorRow, cursorCol),
|
||||
[lines, cursorRow, cursorCol],
|
||||
);
|
||||
|
||||
const returnValue: TextBuffer = useMemo(
|
||||
() => ({
|
||||
lines,
|
||||
@@ -2065,6 +2070,7 @@ export function useTextBuffer({
|
||||
replaceRange,
|
||||
replaceRangeByOffset,
|
||||
moveToOffset,
|
||||
getOffset,
|
||||
moveToVisualPosition,
|
||||
deleteWordLeft,
|
||||
deleteWordRight,
|
||||
@@ -2129,6 +2135,7 @@ export function useTextBuffer({
|
||||
replaceRange,
|
||||
replaceRangeByOffset,
|
||||
moveToOffset,
|
||||
getOffset,
|
||||
moveToVisualPosition,
|
||||
deleteWordLeft,
|
||||
deleteWordRight,
|
||||
@@ -2283,6 +2290,7 @@ export interface TextBuffer {
|
||||
endOffset: number,
|
||||
replacementText: string,
|
||||
) => void;
|
||||
getOffset: () => number;
|
||||
moveToOffset(offset: number): void;
|
||||
moveToVisualPosition(visualRow: number, visualCol: number): void;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('keyMatchers', () => {
|
||||
key.name === 'return' && (key.ctrl || key.meta || key.paste),
|
||||
[Command.OPEN_EXTERNAL_EDITOR]: (key: Key) =>
|
||||
key.ctrl && (key.name === 'x' || key.sequence === '\x18'),
|
||||
[Command.PASTE_CLIPBOARD_IMAGE]: (key: Key) => key.ctrl && key.name === 'v',
|
||||
[Command.PASTE_CLIPBOARD]: (key: Key) => key.ctrl && key.name === 'v',
|
||||
[Command.SHOW_ERROR_DETAILS]: (key: Key) => key.name === 'f12',
|
||||
[Command.SHOW_FULL_TODOS]: (key: Key) => key.ctrl && key.name === 't',
|
||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: (key: Key) =>
|
||||
@@ -268,7 +268,7 @@ describe('keyMatchers', () => {
|
||||
negative: [createKey('x'), createKey('c', { ctrl: true })],
|
||||
},
|
||||
{
|
||||
command: Command.PASTE_CLIPBOARD_IMAGE,
|
||||
command: Command.PASTE_CLIPBOARD,
|
||||
positive: [createKey('v', { ctrl: true })],
|
||||
negative: [createKey('v'), createKey('c', { ctrl: true })],
|
||||
},
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
import type { Mock } from 'vitest';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import type { spawn, SpawnOptions } from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import clipboardy from 'clipboardy';
|
||||
import {
|
||||
isAtCommand,
|
||||
isSlashCommand,
|
||||
@@ -15,6 +15,13 @@ import {
|
||||
getUrlOpenCommand,
|
||||
} from './commandUtils.js';
|
||||
|
||||
// Mock clipboardy
|
||||
vi.mock('clipboardy', () => ({
|
||||
default: {
|
||||
write: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock child_process
|
||||
vi.mock('child_process');
|
||||
|
||||
@@ -44,6 +51,7 @@ interface MockChildProcess extends EventEmitter {
|
||||
describe('commandUtils', () => {
|
||||
let mockSpawn: Mock;
|
||||
let mockChild: MockChildProcess;
|
||||
let mockClipboardyWrite: Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -67,6 +75,9 @@ describe('commandUtils', () => {
|
||||
}) as MockChildProcess;
|
||||
|
||||
mockSpawn.mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
|
||||
|
||||
// Setup clipboardy mock
|
||||
mockClipboardyWrite = clipboardy.write as Mock;
|
||||
});
|
||||
|
||||
describe('isAtCommand', () => {
|
||||
@@ -128,393 +139,23 @@ describe('commandUtils', () => {
|
||||
});
|
||||
|
||||
describe('copyToClipboard', () => {
|
||||
describe('on macOS (darwin)', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'darwin';
|
||||
});
|
||||
it('should successfully copy text to clipboard using clipboardy', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
mockClipboardyWrite.mockResolvedValue(undefined);
|
||||
|
||||
it('should successfully copy text to clipboard using pbcopy', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
await copyToClipboard(testText);
|
||||
|
||||
// Simulate successful execution
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith('pbcopy', []);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle pbcopy command failure', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
// Simulate command failure
|
||||
setTimeout(() => {
|
||||
mockChild.stderr.emit('data', 'Command not found');
|
||||
mockChild.emit('close', 1);
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
"'pbcopy' exited with code 1: Command not found",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle spawn error', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('error', new Error('spawn error'));
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow('spawn error');
|
||||
});
|
||||
|
||||
it('should handle stdin write error', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.stdin.emit('error', new Error('stdin error'));
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow('stdin error');
|
||||
});
|
||||
expect(mockClipboardyWrite).toHaveBeenCalledWith(testText);
|
||||
});
|
||||
|
||||
describe('on Windows (win32)', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'win32';
|
||||
});
|
||||
it('should propagate errors from clipboardy', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
const error = new Error('Clipboard error');
|
||||
mockClipboardyWrite.mockRejectedValue(error);
|
||||
|
||||
it('should successfully copy text to clipboard using clip', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith('clip', []);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Linux', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'linux';
|
||||
});
|
||||
|
||||
it('should successfully copy text to clipboard using xclip', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should successfully copy on Linux when receiving an "exit" event', async () => {
|
||||
const testText = 'Hello, linux!';
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
|
||||
// Simulate successful execution via 'exit' event
|
||||
setTimeout(() => {
|
||||
mockChild.emit('exit', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle command failure on Linux via "exit" event', async () => {
|
||||
const testText = 'Hello, linux!';
|
||||
let callCount = 0;
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
stdout: Object.assign(new EventEmitter(), {
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
stderr: Object.assign(new EventEmitter(), {
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (callCount === 0) {
|
||||
// First call (xclip) fails with 'exit'
|
||||
child.stderr.emit('data', 'xclip failed');
|
||||
child.emit('exit', 127);
|
||||
} else {
|
||||
// Second call (xsel) also fails with 'exit'
|
||||
child.stderr.emit('data', 'xsel failed');
|
||||
child.emit('exit', 127);
|
||||
}
|
||||
callCount++;
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
'All copy commands failed. "\'xclip\' exited with code 127: xclip failed", "\'xsel\' exited with code 127: xsel failed".',
|
||||
);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should fall back to xsel when xclip fails', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
}) as MockChildProcess;
|
||||
|
||||
setTimeout(() => {
|
||||
if (callCount === 0) {
|
||||
// First call (xclip) fails
|
||||
const error = new Error('spawn xclip ENOENT');
|
||||
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
||||
child.emit('error', error);
|
||||
child.emit('close', 1);
|
||||
callCount++;
|
||||
} else {
|
||||
// Second call (xsel) succeeds
|
||||
child.emit('close', 0);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'xsel',
|
||||
['--clipboard', '--input'],
|
||||
linuxOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when both xclip and xsel are not found', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
}) as MockChildProcess;
|
||||
|
||||
setTimeout(() => {
|
||||
if (callCount === 0) {
|
||||
// First call (xclip) fails with ENOENT
|
||||
const error = new Error('spawn xclip ENOENT');
|
||||
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
||||
child.emit('error', error);
|
||||
child.emit('close', 1);
|
||||
callCount++;
|
||||
} else {
|
||||
// Second call (xsel) fails with ENOENT
|
||||
const error = new Error('spawn xsel ENOENT');
|
||||
(error as NodeJS.ErrnoException).code = 'ENOENT';
|
||||
child.emit('error', error);
|
||||
child.emit('close', 1);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
'Please ensure xclip or xsel is installed and configured.',
|
||||
);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'xsel',
|
||||
['--clipboard', '--input'],
|
||||
linuxOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit error when xclip or xsel fail with stderr output (command installed)', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
const linuxOptions: SpawnOptions = {
|
||||
stdio: ['pipe', 'inherit', 'pipe'],
|
||||
};
|
||||
const errorMsg = "Error: Can't open display:";
|
||||
const exitCode = 1;
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
}) as MockChildProcess;
|
||||
|
||||
setTimeout(() => {
|
||||
// e.g., cannot connect to X server
|
||||
if (callCount === 0) {
|
||||
child.stderr.emit('data', errorMsg);
|
||||
child.emit('close', exitCode);
|
||||
callCount++;
|
||||
} else {
|
||||
child.stderr.emit('data', errorMsg);
|
||||
child.emit('close', exitCode);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
|
||||
const xclipErrorMsg = `'xclip' exited with code ${exitCode}${errorMsg ? `: ${errorMsg}` : ''}`;
|
||||
const xselErrorMsg = `'xsel' exited with code ${exitCode}${errorMsg ? `: ${errorMsg}` : ''}`;
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
`All copy commands failed. "${xclipErrorMsg}", "${xselErrorMsg}". `,
|
||||
);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'xclip',
|
||||
['-selection', 'clipboard'],
|
||||
linuxOptions,
|
||||
);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'xsel',
|
||||
['--clipboard', '--input'],
|
||||
linuxOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on unsupported platform', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'unsupported';
|
||||
});
|
||||
|
||||
it('should throw error for unsupported platform', async () => {
|
||||
await expect(copyToClipboard('test')).rejects.toThrow(
|
||||
'Unsupported platform: unsupported',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'darwin';
|
||||
});
|
||||
|
||||
it('should handle command exit without stderr', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 1);
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
"'pbcopy' exited with code 1",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty text', async () => {
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard('');
|
||||
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('should handle multiline text', async () => {
|
||||
const multilineText = 'Line 1\nLine 2\nLine 3';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(multilineText);
|
||||
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(multilineText);
|
||||
});
|
||||
|
||||
it('should handle special characters', async () => {
|
||||
const specialText = 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(specialText);
|
||||
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(specialText);
|
||||
});
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
'Clipboard error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import type { SpawnOptions } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import clipboardy from 'clipboardy';
|
||||
|
||||
/**
|
||||
* Checks if a query string potentially represents an '@' command.
|
||||
@@ -45,109 +44,9 @@ export const isSlashCommand = (query: string): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Copies a string snippet to the clipboard for different platforms
|
||||
// Copies a string snippet to the clipboard
|
||||
export const copyToClipboard = async (text: string): Promise<void> => {
|
||||
const run = (cmd: string, args: string[], options?: SpawnOptions) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const child = options ? spawn(cmd, args, options) : spawn(cmd, args);
|
||||
let stderr = '';
|
||||
if (child.stderr) {
|
||||
child.stderr.on('data', (chunk) => (stderr += chunk.toString()));
|
||||
}
|
||||
const copyResult = (code: number | null) => {
|
||||
if (code === 0) return resolve();
|
||||
const errorMsg = stderr.trim();
|
||||
reject(
|
||||
new Error(
|
||||
`'${cmd}' exited with code ${code}${errorMsg ? `: ${errorMsg}` : ''}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// The 'exit' event workaround is only needed for the specific stdio
|
||||
// configuration used on Linux.
|
||||
if (process.platform === 'linux') {
|
||||
child.on('exit', (code) => {
|
||||
child.stdin?.destroy();
|
||||
child.stdout?.destroy();
|
||||
child.stderr?.destroy();
|
||||
copyResult(code);
|
||||
});
|
||||
}
|
||||
|
||||
child.on('error', reject);
|
||||
|
||||
// For win32/darwin, 'close' is the safest event, guaranteeing all I/O is flushed.
|
||||
// For Linux, this acts as a fallback. This is safe because the promise
|
||||
// can only be settled once.
|
||||
child.on('close', (code) => {
|
||||
copyResult(code);
|
||||
});
|
||||
|
||||
if (child.stdin) {
|
||||
child.stdin.on('error', reject);
|
||||
child.stdin.write(text);
|
||||
child.stdin.end();
|
||||
} else {
|
||||
reject(new Error('Child process has no stdin stream to write to.'));
|
||||
}
|
||||
});
|
||||
|
||||
// Configure stdio for Linux clipboard commands.
|
||||
// - stdin: 'pipe' to write the text that needs to be copied.
|
||||
// - stdout: 'inherit' since we don't need to capture the command's output on success.
|
||||
// - stderr: 'pipe' to capture error messages (e.g., "command not found") for better error handling.
|
||||
const linuxOptions: SpawnOptions = { stdio: ['pipe', 'inherit', 'pipe'] };
|
||||
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return run('clip', []);
|
||||
case 'darwin':
|
||||
return run('pbcopy', []);
|
||||
case 'linux':
|
||||
try {
|
||||
await run('xclip', ['-selection', 'clipboard'], linuxOptions);
|
||||
} catch (primaryError) {
|
||||
try {
|
||||
// If xclip fails for any reason, try xsel as a fallback.
|
||||
await run('xsel', ['--clipboard', '--input'], linuxOptions);
|
||||
} catch (fallbackError) {
|
||||
const xclipNotFound =
|
||||
primaryError instanceof Error &&
|
||||
(primaryError as NodeJS.ErrnoException).code === 'ENOENT';
|
||||
const xselNotFound =
|
||||
fallbackError instanceof Error &&
|
||||
(fallbackError as NodeJS.ErrnoException).code === 'ENOENT';
|
||||
if (xclipNotFound && xselNotFound) {
|
||||
throw new Error(
|
||||
'Please ensure xclip or xsel is installed and configured.',
|
||||
);
|
||||
}
|
||||
|
||||
let primaryMsg =
|
||||
primaryError instanceof Error
|
||||
? primaryError.message
|
||||
: String(primaryError);
|
||||
if (xclipNotFound) {
|
||||
primaryMsg = `xclip not found`;
|
||||
}
|
||||
let fallbackMsg =
|
||||
fallbackError instanceof Error
|
||||
? fallbackError.message
|
||||
: String(fallbackError);
|
||||
if (xselNotFound) {
|
||||
fallbackMsg = `xsel not found`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`All copy commands failed. "${primaryMsg}", "${fallbackMsg}". `,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
await clipboardy.write(text);
|
||||
};
|
||||
|
||||
export const getUrlOpenCommand = (): string => {
|
||||
|
||||
Reference in New Issue
Block a user