mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-29 14:34:55 -07:00
feat(cli): truncate shell output in UI history and improve active shell display (#17438)
This commit is contained in:
Generated
+1
-24
@@ -2253,7 +2253,6 @@
|
|||||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^6.0.0",
|
"@octokit/auth-token": "^6.0.0",
|
||||||
"@octokit/graphql": "^9.0.2",
|
"@octokit/graphql": "^9.0.2",
|
||||||
@@ -2434,7 +2433,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -2468,7 +2466,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
},
|
},
|
||||||
@@ -2837,7 +2834,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||||
@@ -2871,7 +2867,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1"
|
"@opentelemetry/resources": "2.0.1"
|
||||||
@@ -2924,7 +2919,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/core": "2.0.1",
|
"@opentelemetry/core": "2.0.1",
|
||||||
"@opentelemetry/resources": "2.0.1",
|
"@opentelemetry/resources": "2.0.1",
|
||||||
@@ -4140,7 +4134,6 @@
|
|||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -4435,7 +4428,6 @@
|
|||||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.35.0",
|
"@typescript-eslint/scope-manager": "8.35.0",
|
||||||
"@typescript-eslint/types": "8.35.0",
|
"@typescript-eslint/types": "8.35.0",
|
||||||
@@ -5428,7 +5420,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -8438,7 +8429,6 @@
|
|||||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -8979,7 +8969,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@@ -10581,7 +10570,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
@@ -14366,7 +14354,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -14377,7 +14364,6 @@
|
|||||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shell-quote": "^1.6.1",
|
"shell-quote": "^1.6.1",
|
||||||
"ws": "^7"
|
"ws": "^7"
|
||||||
@@ -16614,7 +16600,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -16838,8 +16823,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD",
|
"license": "0BSD"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.20.3",
|
"version": "4.20.3",
|
||||||
@@ -16847,7 +16831,6 @@
|
|||||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.25.0",
|
"esbuild": "~0.25.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
@@ -17020,7 +17003,6 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -17228,7 +17210,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -17342,7 +17323,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -17355,7 +17335,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
@@ -18060,7 +18039,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -18357,7 +18335,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ describe('<AnsiOutputText />', () => {
|
|||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toBeDefined();
|
expect(output).toBeDefined();
|
||||||
const lines = output!.split('\n');
|
const lines = output!.split('\n');
|
||||||
expect(lines[0]).toBe('First line');
|
expect(lines[0].trim()).toBe('First line');
|
||||||
expect(lines[1]).toBe('Third line');
|
expect(lines[1].trim()).toBe('');
|
||||||
|
expect(lines[2].trim()).toBe('Third line');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the availableTerminalHeight prop and slices the lines correctly', () => {
|
it('respects the availableTerminalHeight prop and slices the lines correctly', () => {
|
||||||
@@ -89,6 +90,45 @@ describe('<AnsiOutputText />', () => {
|
|||||||
expect(output).toContain('Line 4');
|
expect(output).toContain('Line 4');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('respects the maxLines prop and slices the lines correctly', () => {
|
||||||
|
const data: AnsiOutput = [
|
||||||
|
[createAnsiToken({ text: 'Line 1' })],
|
||||||
|
[createAnsiToken({ text: 'Line 2' })],
|
||||||
|
[createAnsiToken({ text: 'Line 3' })],
|
||||||
|
[createAnsiToken({ text: 'Line 4' })],
|
||||||
|
];
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<AnsiOutputText data={data} maxLines={2} width={80} />,
|
||||||
|
);
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).not.toContain('Line 1');
|
||||||
|
expect(output).not.toContain('Line 2');
|
||||||
|
expect(output).toContain('Line 3');
|
||||||
|
expect(output).toContain('Line 4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prioritizes maxLines over availableTerminalHeight if maxLines is smaller', () => {
|
||||||
|
const data: AnsiOutput = [
|
||||||
|
[createAnsiToken({ text: 'Line 1' })],
|
||||||
|
[createAnsiToken({ text: 'Line 2' })],
|
||||||
|
[createAnsiToken({ text: 'Line 3' })],
|
||||||
|
[createAnsiToken({ text: 'Line 4' })],
|
||||||
|
];
|
||||||
|
// availableTerminalHeight=3, maxLines=2 => show 2 lines
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<AnsiOutputText
|
||||||
|
data={data}
|
||||||
|
availableTerminalHeight={3}
|
||||||
|
maxLines={2}
|
||||||
|
width={80}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).not.toContain('Line 2');
|
||||||
|
expect(output).toContain('Line 3');
|
||||||
|
expect(output).toContain('Line 4');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders a large AnsiOutput object without crashing', () => {
|
it('renders a large AnsiOutput object without crashing', () => {
|
||||||
const largeData: AnsiOutput = [];
|
const largeData: AnsiOutput = [];
|
||||||
for (let i = 0; i < 1000; i++) {
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
|||||||
@@ -14,40 +14,56 @@ interface AnsiOutputProps {
|
|||||||
data: AnsiOutput;
|
data: AnsiOutput;
|
||||||
availableTerminalHeight?: number;
|
availableTerminalHeight?: number;
|
||||||
width: number;
|
width: number;
|
||||||
|
maxLines?: number;
|
||||||
|
disableTruncation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnsiOutputText: React.FC<AnsiOutputProps> = ({
|
export const AnsiOutputText: React.FC<AnsiOutputProps> = ({
|
||||||
data,
|
data,
|
||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
width,
|
width,
|
||||||
|
maxLines,
|
||||||
|
disableTruncation,
|
||||||
}) => {
|
}) => {
|
||||||
const lastLines = data.slice(
|
const availableHeightLimit =
|
||||||
-(availableTerminalHeight && availableTerminalHeight > 0
|
availableTerminalHeight && availableTerminalHeight > 0
|
||||||
? availableTerminalHeight
|
? availableTerminalHeight
|
||||||
: DEFAULT_HEIGHT),
|
: undefined;
|
||||||
);
|
|
||||||
|
const numLinesRetained =
|
||||||
|
availableHeightLimit !== undefined && maxLines !== undefined
|
||||||
|
? Math.min(availableHeightLimit, maxLines)
|
||||||
|
: (availableHeightLimit ?? maxLines ?? DEFAULT_HEIGHT);
|
||||||
|
|
||||||
|
const lastLines = disableTruncation ? data : data.slice(-numLinesRetained);
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" width={width} flexShrink={0}>
|
<Box flexDirection="column" width={width} flexShrink={0} overflow="hidden">
|
||||||
{lastLines.map((line: AnsiLine, lineIndex: number) => (
|
{lastLines.map((line: AnsiLine, lineIndex: number) => (
|
||||||
<Text key={lineIndex} wrap="truncate">
|
<Box key={lineIndex} height={1} overflow="hidden">
|
||||||
{line.length > 0
|
<AnsiLineText line={line} />
|
||||||
? line.map((token: AnsiToken, tokenIndex: number) => (
|
</Box>
|
||||||
<Text
|
|
||||||
key={tokenIndex}
|
|
||||||
color={token.fg}
|
|
||||||
backgroundColor={token.bg}
|
|
||||||
inverse={token.inverse}
|
|
||||||
dimColor={token.dim}
|
|
||||||
bold={token.bold}
|
|
||||||
italic={token.italic}
|
|
||||||
underline={token.underline}
|
|
||||||
>
|
|
||||||
{token.text}
|
|
||||||
</Text>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</Text>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AnsiLineText: React.FC<{ line: AnsiLine }> = ({ line }) => (
|
||||||
|
<Text>
|
||||||
|
{line.length > 0
|
||||||
|
? line.map((token: AnsiToken, tokenIndex: number) => (
|
||||||
|
<Text
|
||||||
|
key={tokenIndex}
|
||||||
|
color={token.fg}
|
||||||
|
backgroundColor={token.bg}
|
||||||
|
inverse={token.inverse}
|
||||||
|
dimColor={token.dim}
|
||||||
|
bold={token.bold}
|
||||||
|
italic={token.italic}
|
||||||
|
underline={token.underline}
|
||||||
|
>
|
||||||
|
{token.text}
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { MainContent } from './MainContent.js';
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||||
|
import { ToolCallStatus } from '../types.js';
|
||||||
|
import { SHELL_COMMAND_NAME } from '../constants.js';
|
||||||
|
import type { UIState } from '../contexts/UIStateContext.js';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('../contexts/AppContext.js', async () => {
|
vi.mock('../contexts/AppContext.js', async () => {
|
||||||
@@ -22,53 +26,10 @@ vi.mock('../contexts/AppContext.js', async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../contexts/UIStateContext.js', async () => {
|
|
||||||
const actual = await vi.importActual('../contexts/UIStateContext.js');
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useUIState: () => ({
|
|
||||||
history: [
|
|
||||||
{ id: 1, role: 'user', content: 'Hello' },
|
|
||||||
{ id: 2, role: 'model', content: 'Hi there' },
|
|
||||||
],
|
|
||||||
pendingHistoryItems: [],
|
|
||||||
mainAreaWidth: 80,
|
|
||||||
staticAreaMaxItemHeight: 20,
|
|
||||||
availableTerminalHeight: 24,
|
|
||||||
slashCommands: [],
|
|
||||||
constrainHeight: false,
|
|
||||||
isEditorDialogOpen: false,
|
|
||||||
activePtyId: undefined,
|
|
||||||
embeddedShellFocused: false,
|
|
||||||
historyRemountKey: 0,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
||||||
useAlternateBuffer: vi.fn(),
|
useAlternateBuffer: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./HistoryItemDisplay.js', () => ({
|
|
||||||
HistoryItemDisplay: ({
|
|
||||||
item,
|
|
||||||
availableTerminalHeight,
|
|
||||||
}: {
|
|
||||||
item: { content: string };
|
|
||||||
availableTerminalHeight?: number;
|
|
||||||
}) => (
|
|
||||||
<Box>
|
|
||||||
<Text>
|
|
||||||
HistoryItem: {item.content} (height:{' '}
|
|
||||||
{availableTerminalHeight === undefined
|
|
||||||
? 'undefined'
|
|
||||||
: availableTerminalHeight}
|
|
||||||
)
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('./AppHeader.js', () => ({
|
vi.mock('./AppHeader.js', () => ({
|
||||||
AppHeader: () => <Text>AppHeader</Text>,
|
AppHeader: () => <Text>AppHeader</Text>,
|
||||||
}));
|
}));
|
||||||
@@ -95,39 +56,169 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
|||||||
SCROLL_TO_ITEM_END: 0,
|
SCROLL_TO_ITEM_END: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
|
||||||
|
|
||||||
describe('MainContent', () => {
|
describe('MainContent', () => {
|
||||||
|
const defaultMockUiState = {
|
||||||
|
history: [
|
||||||
|
{ id: 1, type: 'user', text: 'Hello' },
|
||||||
|
{ id: 2, type: 'gemini', text: 'Hi there' },
|
||||||
|
],
|
||||||
|
pendingHistoryItems: [],
|
||||||
|
mainAreaWidth: 80,
|
||||||
|
staticAreaMaxItemHeight: 20,
|
||||||
|
availableTerminalHeight: 24,
|
||||||
|
slashCommands: [],
|
||||||
|
constrainHeight: false,
|
||||||
|
isEditorDialogOpen: false,
|
||||||
|
activePtyId: undefined,
|
||||||
|
embeddedShellFocused: false,
|
||||||
|
historyRemountKey: 0,
|
||||||
|
bannerData: { defaultText: '', warningText: '' },
|
||||||
|
bannerVisible: false,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(useAlternateBuffer).mockReturnValue(false);
|
vi.mocked(useAlternateBuffer).mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders in normal buffer mode', async () => {
|
it('renders in normal buffer mode', async () => {
|
||||||
const { lastFrame } = renderWithProviders(<MainContent />);
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
|
uiState: defaultMockUiState as Partial<UIState>,
|
||||||
|
});
|
||||||
await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
|
await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
expect(output).toContain('HistoryItem: Hello (height: 20)');
|
expect(output).toContain('Hello');
|
||||||
expect(output).toContain('HistoryItem: Hi there (height: 20)');
|
expect(output).toContain('Hi there');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders in alternate buffer mode', async () => {
|
it('renders in alternate buffer mode', async () => {
|
||||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||||
const { lastFrame } = renderWithProviders(<MainContent />);
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
|
uiState: defaultMockUiState as Partial<UIState>,
|
||||||
|
});
|
||||||
await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
|
await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
expect(output).toContain('AppHeader');
|
expect(output).toContain('AppHeader');
|
||||||
expect(output).toContain('HistoryItem: Hello (height: undefined)');
|
expect(output).toContain('Hello');
|
||||||
expect(output).toContain('HistoryItem: Hi there (height: undefined)');
|
expect(output).toContain('Hi there');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not constrain height in alternate buffer mode', async () => {
|
it('does not constrain height in alternate buffer mode', async () => {
|
||||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||||
const { lastFrame } = renderWithProviders(<MainContent />);
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
await waitFor(() => expect(lastFrame()).toContain('HistoryItem: Hello'));
|
uiState: defaultMockUiState as Partial<UIState>,
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(lastFrame()).toContain('Hello'));
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
expect(output).toMatchSnapshot();
|
expect(output).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('MainContent Tool Output Height Logic', () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
name: 'ASB mode - Focused shell should expand',
|
||||||
|
isAlternateBuffer: true,
|
||||||
|
embeddedShellFocused: true,
|
||||||
|
constrainHeight: true,
|
||||||
|
shouldShowLine1: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ASB mode - Unfocused shell',
|
||||||
|
isAlternateBuffer: true,
|
||||||
|
embeddedShellFocused: false,
|
||||||
|
constrainHeight: true,
|
||||||
|
shouldShowLine1: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Normal mode - Constrained height',
|
||||||
|
isAlternateBuffer: false,
|
||||||
|
embeddedShellFocused: false,
|
||||||
|
constrainHeight: true,
|
||||||
|
shouldShowLine1: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Normal mode - Unconstrained height',
|
||||||
|
isAlternateBuffer: false,
|
||||||
|
embeddedShellFocused: false,
|
||||||
|
constrainHeight: false,
|
||||||
|
shouldShowLine1: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(testCases)(
|
||||||
|
'$name',
|
||||||
|
async ({
|
||||||
|
isAlternateBuffer,
|
||||||
|
embeddedShellFocused,
|
||||||
|
constrainHeight,
|
||||||
|
shouldShowLine1,
|
||||||
|
}) => {
|
||||||
|
vi.mocked(useAlternateBuffer).mockReturnValue(isAlternateBuffer);
|
||||||
|
const ptyId = 123;
|
||||||
|
const uiState = {
|
||||||
|
history: [],
|
||||||
|
pendingHistoryItems: [
|
||||||
|
{
|
||||||
|
type: 'tool_group' as const,
|
||||||
|
id: 1,
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
callId: 'call_1',
|
||||||
|
name: SHELL_COMMAND_NAME,
|
||||||
|
status: ToolCallStatus.Executing,
|
||||||
|
description: 'Running a long command...',
|
||||||
|
// 20 lines of output.
|
||||||
|
// Default max is 15, so Line 1-5 will be truncated/scrolled out if not expanded.
|
||||||
|
resultDisplay: Array.from(
|
||||||
|
{ length: 20 },
|
||||||
|
(_, i) => `Line ${i + 1}`,
|
||||||
|
).join('\n'),
|
||||||
|
ptyId,
|
||||||
|
confirmationDetails: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableTerminalHeight: 30, // In ASB mode, focused shell should get ~28 lines
|
||||||
|
terminalHeight: 50,
|
||||||
|
terminalWidth: 100,
|
||||||
|
mainAreaWidth: 100,
|
||||||
|
embeddedShellFocused,
|
||||||
|
activePtyId: embeddedShellFocused ? ptyId : undefined,
|
||||||
|
constrainHeight,
|
||||||
|
isEditorDialogOpen: false,
|
||||||
|
slashCommands: [],
|
||||||
|
historyRemountKey: 0,
|
||||||
|
bannerData: {
|
||||||
|
defaultText: '',
|
||||||
|
warningText: '',
|
||||||
|
},
|
||||||
|
bannerVisible: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
|
uiState: uiState as Partial<UIState>,
|
||||||
|
useAlternateBuffer: isAlternateBuffer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = lastFrame();
|
||||||
|
|
||||||
|
// Sanity checks - Use regex with word boundary to avoid matching "Line 10" etc.
|
||||||
|
const line1Regex = /\bLine 1\b/;
|
||||||
|
if (shouldShowLine1) {
|
||||||
|
expect(output).toMatch(line1Regex);
|
||||||
|
} else {
|
||||||
|
expect(output).not.toMatch(line1Regex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All cases should show the last line
|
||||||
|
expect(output).toContain('Line 20');
|
||||||
|
|
||||||
|
// Snapshots for visual verification
|
||||||
|
expect(output).toMatchSnapshot();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export const MainContent = () => {
|
|||||||
<HistoryItemDisplay
|
<HistoryItemDisplay
|
||||||
key={i}
|
key={i}
|
||||||
availableTerminalHeight={
|
availableTerminalHeight={
|
||||||
uiState.constrainHeight && !isAlternateBuffer
|
(uiState.constrainHeight && !isAlternateBuffer) ||
|
||||||
|
isAlternateBuffer
|
||||||
? availableTerminalHeight
|
? availableTerminalHeight
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -148,7 +149,7 @@ export const MainContent = () => {
|
|||||||
return (
|
return (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
ref={scrollableListRef}
|
ref={scrollableListRef}
|
||||||
hasFocus={!uiState.isEditorDialogOpen}
|
hasFocus={!uiState.isEditorDialogOpen && !uiState.embeddedShellFocused}
|
||||||
width={uiState.terminalWidth}
|
width={uiState.terminalWidth}
|
||||||
data={virtualizedData}
|
data={virtualizedData}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { render, persistentStateMock } from '../../test-utils/render.js';
|
import { render, persistentStateMock } from '../../test-utils/render.js';
|
||||||
|
import { waitFor } from '../../test-utils/async.js';
|
||||||
import { Notifications } from './Notifications.js';
|
import { Notifications } from './Notifications.js';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { useAppContext, type AppState } from '../contexts/AppContext.js';
|
import { useAppContext, type AppState } from '../contexts/AppContext.js';
|
||||||
@@ -172,7 +173,7 @@ describe('Notifications', () => {
|
|||||||
render(<Notifications />);
|
render(<Notifications />);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await vi.waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||||
'hasSeenScreenReaderNudge',
|
'hasSeenScreenReaderNudge',
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -95,16 +95,64 @@ describe('ShellInputPrompt', () => {
|
|||||||
it.each([
|
it.each([
|
||||||
['up', -1],
|
['up', -1],
|
||||||
['down', 1],
|
['down', 1],
|
||||||
])('handles scroll %s (Ctrl+Shift+%s)', (key, direction) => {
|
])('handles scroll %s (Command.SCROLL_%s)', (key, direction) => {
|
||||||
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||||
|
|
||||||
const handler = mockUseKeypress.mock.calls[0][0];
|
const handler = mockUseKeypress.mock.calls[0][0];
|
||||||
|
|
||||||
handler({ name: key, shift: true, alt: false, ctrl: true, cmd: false });
|
handler({ name: key, shift: true, alt: false, ctrl: false, cmd: false });
|
||||||
|
|
||||||
expect(mockScrollPty).toHaveBeenCalledWith(1, direction);
|
expect(mockScrollPty).toHaveBeenCalledWith(1, direction);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['pageup', -15],
|
||||||
|
['pagedown', 15],
|
||||||
|
])(
|
||||||
|
'handles page scroll %s (Command.PAGE_%s) with default size',
|
||||||
|
(key, expectedScroll) => {
|
||||||
|
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||||
|
|
||||||
|
const handler = mockUseKeypress.mock.calls[0][0];
|
||||||
|
|
||||||
|
handler({ name: key, shift: false, alt: false, ctrl: false, cmd: false });
|
||||||
|
|
||||||
|
expect(mockScrollPty).toHaveBeenCalledWith(1, expectedScroll);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('respects scrollPageSize prop', () => {
|
||||||
|
render(
|
||||||
|
<ShellInputPrompt
|
||||||
|
activeShellPtyId={1}
|
||||||
|
focus={true}
|
||||||
|
scrollPageSize={10}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = mockUseKeypress.mock.calls[0][0];
|
||||||
|
|
||||||
|
// PageDown
|
||||||
|
handler({
|
||||||
|
name: 'pagedown',
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: false,
|
||||||
|
cmd: false,
|
||||||
|
});
|
||||||
|
expect(mockScrollPty).toHaveBeenCalledWith(1, 10);
|
||||||
|
|
||||||
|
// PageUp
|
||||||
|
handler({
|
||||||
|
name: 'pageup',
|
||||||
|
shift: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: false,
|
||||||
|
cmd: false,
|
||||||
|
});
|
||||||
|
expect(mockScrollPty).toHaveBeenCalledWith(1, -10);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not handle input when not focused', () => {
|
it('does not handle input when not focused', () => {
|
||||||
render(<ShellInputPrompt activeShellPtyId={1} focus={false} />);
|
render(<ShellInputPrompt activeShellPtyId={1} focus={false} />);
|
||||||
|
|
||||||
@@ -138,4 +186,21 @@ describe('ShellInputPrompt', () => {
|
|||||||
|
|
||||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores Command.UNFOCUS_SHELL (Shift+Tab) to allow focus navigation', () => {
|
||||||
|
render(<ShellInputPrompt activeShellPtyId={1} focus={true} />);
|
||||||
|
|
||||||
|
const handler = mockUseKeypress.mock.calls[0][0];
|
||||||
|
|
||||||
|
const result = handler({
|
||||||
|
name: 'tab',
|
||||||
|
shift: true,
|
||||||
|
alt: false,
|
||||||
|
ctrl: false,
|
||||||
|
cmd: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,16 +9,19 @@ import type React from 'react';
|
|||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||||
import { keyToAnsi, type Key } from '../hooks/keyToAnsi.js';
|
import { keyToAnsi, type Key } from '../hooks/keyToAnsi.js';
|
||||||
|
import { ACTIVE_SHELL_MAX_LINES } from '../constants.js';
|
||||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
import { Command, keyMatchers } from '../keyMatchers.js';
|
||||||
|
|
||||||
export interface ShellInputPromptProps {
|
export interface ShellInputPromptProps {
|
||||||
activeShellPtyId: number | null;
|
activeShellPtyId: number | null;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
|
scrollPageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
||||||
activeShellPtyId,
|
activeShellPtyId,
|
||||||
focus = true,
|
focus = true,
|
||||||
|
scrollPageSize = ACTIVE_SHELL_MAX_LINES,
|
||||||
}) => {
|
}) => {
|
||||||
const handleShellInputSubmit = useCallback(
|
const handleShellInputSubmit = useCallback(
|
||||||
(input: string) => {
|
(input: string) => {
|
||||||
@@ -34,26 +37,33 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
|||||||
if (!focus || !activeShellPtyId) {
|
if (!focus || !activeShellPtyId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow background shell toggle to bubble up
|
// Allow background shell toggle to bubble up
|
||||||
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
|
if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow unfocus to bubble up
|
// Allow Shift+Tab to bubble up for focus navigation
|
||||||
if (keyMatchers[Command.UNFOCUS_SHELL_INPUT](key)) {
|
if (keyMatchers[Command.UNFOCUS_SHELL_INPUT](key)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.ctrl && key.shift && key.name === 'up') {
|
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||||
ShellExecutionService.scrollPty(activeShellPtyId, -1);
|
ShellExecutionService.scrollPty(activeShellPtyId, -1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||||
if (key.ctrl && key.shift && key.name === 'down') {
|
|
||||||
ShellExecutionService.scrollPty(activeShellPtyId, 1);
|
ShellExecutionService.scrollPty(activeShellPtyId, 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// TODO: Check pty service actually scrolls (request)[https://github.com/google-gemini/gemini-cli/pull/17438/changes/c9fdaf8967da0036bfef43592fcab5a69537df35#r2776479023].
|
||||||
|
if (keyMatchers[Command.PAGE_UP](key)) {
|
||||||
|
ShellExecutionService.scrollPty(activeShellPtyId, -scrollPageSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (keyMatchers[Command.PAGE_DOWN](key)) {
|
||||||
|
ShellExecutionService.scrollPty(activeShellPtyId, scrollPageSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const ansiSequence = keyToAnsi(key);
|
const ansiSequence = keyToAnsi(key);
|
||||||
if (ansiSequence) {
|
if (ansiSequence) {
|
||||||
@@ -63,7 +73,7 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[focus, handleShellInputSubmit, activeShellPtyId],
|
[focus, handleShellInputSubmit, activeShellPtyId, scrollPageSize],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleInput, { isActive: focus });
|
useKeypress(handleInput, { isActive: focus });
|
||||||
|
|||||||
+16
-16
@@ -39,14 +39,14 @@ Tips for getting started:
|
|||||||
2. Be specific for the best results.
|
2. Be specific for the best results.
|
||||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||||
4. /help for more information.
|
4. /help for more information.
|
||||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool1 Description for tool 1 │
|
│ ✓ tool1 Description for tool 1 │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────╯
|
||||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool2 Description for tool 2 │
|
│ ✓ tool2 Description for tool 2 │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
|
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
|
||||||
@@ -83,14 +83,14 @@ Tips for getting started:
|
|||||||
2. Be specific for the best results.
|
2. Be specific for the best results.
|
||||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||||
4. /help for more information.
|
4. /help for more information.
|
||||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool1 Description for tool 1 │
|
│ ✓ tool1 Description for tool 1 │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────╯
|
||||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool2 Description for tool 2 │
|
│ ✓ tool2 Description for tool 2 │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
|
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
|
||||||
|
|||||||
@@ -1,8 +1,116 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focused shell should expand' 1`] = `
|
||||||
|
"ScrollableList
|
||||||
|
AppHeader
|
||||||
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command Running a long command... │
|
||||||
|
│ │
|
||||||
|
│ Line 1 │
|
||||||
|
│ Line 2 │
|
||||||
|
│ Line 3 │
|
||||||
|
│ Line 4 │
|
||||||
|
│ Line 5 │
|
||||||
|
│ Line 6 │
|
||||||
|
│ Line 7 │
|
||||||
|
│ Line 8 │
|
||||||
|
│ Line 9 │
|
||||||
|
│ Line 10 │
|
||||||
|
│ Line 11 │
|
||||||
|
│ Line 12 │
|
||||||
|
│ Line 13 │
|
||||||
|
│ Line 14 │
|
||||||
|
│ Line 15 │
|
||||||
|
│ Line 16 │
|
||||||
|
│ Line 17 │
|
||||||
|
│ Line 18 │
|
||||||
|
│ Line 19 │
|
||||||
|
│ Line 20 │
|
||||||
|
│ │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
ShowMoreLines"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocused shell' 1`] = `
|
||||||
|
"ScrollableList
|
||||||
|
AppHeader
|
||||||
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command Running a long command... │
|
||||||
|
│ │
|
||||||
|
│ Line 6 │
|
||||||
|
│ Line 7 │
|
||||||
|
│ Line 8 │
|
||||||
|
│ Line 9 ▄ │
|
||||||
|
│ Line 10 █ │
|
||||||
|
│ Line 11 █ │
|
||||||
|
│ Line 12 █ │
|
||||||
|
│ Line 13 █ │
|
||||||
|
│ Line 14 █ │
|
||||||
|
│ Line 15 █ │
|
||||||
|
│ Line 16 █ │
|
||||||
|
│ Line 17 █ │
|
||||||
|
│ Line 18 █ │
|
||||||
|
│ Line 19 █ │
|
||||||
|
│ Line 20 █ │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
ShowMoreLines"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = `
|
||||||
|
"AppHeader
|
||||||
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command Running a long command... │
|
||||||
|
│ │
|
||||||
|
│ Line 6 │
|
||||||
|
│ Line 7 │
|
||||||
|
│ Line 8 │
|
||||||
|
│ Line 9 │
|
||||||
|
│ Line 10 │
|
||||||
|
│ Line 11 │
|
||||||
|
│ Line 12 │
|
||||||
|
│ Line 13 │
|
||||||
|
│ Line 14 │
|
||||||
|
│ Line 15 │
|
||||||
|
│ Line 16 │
|
||||||
|
│ Line 17 │
|
||||||
|
│ Line 18 │
|
||||||
|
│ Line 19 │
|
||||||
|
│ Line 20 │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
ShowMoreLines"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = `
|
||||||
|
"AppHeader
|
||||||
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command Running a long command... │
|
||||||
|
│ │
|
||||||
|
│ Line 6 │
|
||||||
|
│ Line 7 │
|
||||||
|
│ Line 8 │
|
||||||
|
│ Line 9 │
|
||||||
|
│ Line 10 │
|
||||||
|
│ Line 11 │
|
||||||
|
│ Line 12 │
|
||||||
|
│ Line 13 │
|
||||||
|
│ Line 14 │
|
||||||
|
│ Line 15 │
|
||||||
|
│ Line 16 │
|
||||||
|
│ Line 17 │
|
||||||
|
│ Line 18 │
|
||||||
|
│ Line 19 │
|
||||||
|
│ Line 20 │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
ShowMoreLines"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`MainContent > does not constrain height in alternate buffer mode 1`] = `
|
exports[`MainContent > does not constrain height in alternate buffer mode 1`] = `
|
||||||
"ScrollableList
|
"ScrollableList
|
||||||
AppHeader
|
AppHeader
|
||||||
HistoryItem: Hello (height: undefined)
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
HistoryItem: Hi there (height: undefined)"
|
> Hello
|
||||||
|
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||||
|
✦ Hi there
|
||||||
|
ShowMoreLines
|
||||||
|
"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -4,55 +4,18 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { act } from 'react';
|
||||||
import {
|
import {
|
||||||
ShellToolMessage,
|
ShellToolMessage,
|
||||||
type ShellToolMessageProps,
|
type ShellToolMessageProps,
|
||||||
} from './ShellToolMessage.js';
|
} from './ShellToolMessage.js';
|
||||||
import { StreamingState, ToolCallStatus } from '../../types.js';
|
import { StreamingState, ToolCallStatus } from '../../types.js';
|
||||||
import { Text } from 'ink';
|
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
import { waitFor } from '../../../test-utils/async.js';
|
import { waitFor } from '../../../test-utils/async.js';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
||||||
import { SHELL_COMMAND_NAME } from '../../constants.js';
|
import { SHELL_COMMAND_NAME, ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
|
||||||
import { StreamingContext } from '../../contexts/StreamingContext.js';
|
|
||||||
|
|
||||||
vi.mock('../TerminalOutput.js', () => ({
|
|
||||||
TerminalOutput: function MockTerminalOutput({
|
|
||||||
cursor,
|
|
||||||
}: {
|
|
||||||
cursor: { x: number; y: number } | null;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Text>
|
|
||||||
MockCursor:({cursor?.x},{cursor?.y})
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock child components or utilities if they are complex or have side effects
|
|
||||||
vi.mock('../GeminiRespondingSpinner.js', () => ({
|
|
||||||
GeminiRespondingSpinner: ({
|
|
||||||
nonRespondingDisplay,
|
|
||||||
}: {
|
|
||||||
nonRespondingDisplay?: string;
|
|
||||||
}) => {
|
|
||||||
const streamingState = React.useContext(StreamingContext)!;
|
|
||||||
if (streamingState === StreamingState.Responding) {
|
|
||||||
return <Text>MockRespondingSpinner</Text>;
|
|
||||||
}
|
|
||||||
return nonRespondingDisplay ? <Text>{nonRespondingDisplay}</Text> : null;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
|
||||||
MarkdownDisplay: function MockMarkdownDisplay({ text }: { text: string }) {
|
|
||||||
return <Text>MockMarkdown:{text}</Text>;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('<ShellToolMessage />', () => {
|
describe('<ShellToolMessage />', () => {
|
||||||
const baseProps: ShellToolMessageProps = {
|
const baseProps: ShellToolMessageProps = {
|
||||||
@@ -72,52 +35,36 @@ describe('<ShellToolMessage />', () => {
|
|||||||
} as unknown as Config,
|
} as unknown as Config,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LONG_OUTPUT = Array.from(
|
||||||
|
{ length: 100 },
|
||||||
|
(_, i) => `Line ${i + 1}`,
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
const mockSetEmbeddedShellFocused = vi.fn();
|
const mockSetEmbeddedShellFocused = vi.fn();
|
||||||
const uiActions = {
|
const uiActions = {
|
||||||
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
|
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderShell = (
|
||||||
|
props: Partial<ShellToolMessageProps> = {},
|
||||||
|
options: Parameters<typeof renderWithProviders>[1] = {},
|
||||||
|
) =>
|
||||||
|
renderWithProviders(<ShellToolMessage {...baseProps} {...props} />, {
|
||||||
|
uiActions,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('interactive shell focus', () => {
|
describe('interactive shell focus', () => {
|
||||||
const shellProps: ShellToolMessageProps = {
|
it.each([
|
||||||
...baseProps,
|
['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME],
|
||||||
};
|
['SHELL_TOOL_NAME', SHELL_TOOL_NAME],
|
||||||
|
])('clicks inside the shell area sets focus for %s', async (_, name) => {
|
||||||
it('clicks inside the shell area sets focus to true', async () => {
|
const { stdin, lastFrame, simulateClick } = renderShell(
|
||||||
const { stdin, lastFrame, simulateClick } = renderWithProviders(
|
{ name },
|
||||||
<ShellToolMessage {...shellProps} />,
|
{ mouseEventsEnabled: true },
|
||||||
{
|
|
||||||
mouseEventsEnabled: true,
|
|
||||||
uiActions,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(lastFrame()).toContain('A shell command'); // Wait for render
|
|
||||||
});
|
|
||||||
|
|
||||||
await simulateClick(stdin, 2, 2); // Click at column 2, row 2 (1-based)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles focus for SHELL_TOOL_NAME (core shell tool)', async () => {
|
|
||||||
const coreShellProps: ShellToolMessageProps = {
|
|
||||||
...shellProps,
|
|
||||||
name: SHELL_TOOL_NAME,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { stdin, lastFrame, simulateClick } = renderWithProviders(
|
|
||||||
<ShellToolMessage {...coreShellProps} />,
|
|
||||||
{
|
|
||||||
mouseEventsEnabled: true,
|
|
||||||
uiActions,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -130,5 +77,136 @@ describe('<ShellToolMessage />', () => {
|
|||||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('resets focus when shell finishes', async () => {
|
||||||
|
let updateStatus: (s: ToolCallStatus) => void = () => {};
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
const [status, setStatus] = React.useState(ToolCallStatus.Executing);
|
||||||
|
updateStatus = setStatus;
|
||||||
|
return (
|
||||||
|
<ShellToolMessage
|
||||||
|
{...baseProps}
|
||||||
|
status={status}
|
||||||
|
embeddedShellFocused={true}
|
||||||
|
activeShellPtyId={1}
|
||||||
|
ptyId={1}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(<Wrapper />, {
|
||||||
|
uiActions,
|
||||||
|
uiState: { streamingState: StreamingState.Idle },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify it is initially focused
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(lastFrame()).toContain('(Shift+Tab to unfocus)');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now update status to Success
|
||||||
|
await act(async () => {
|
||||||
|
updateStatus(ToolCallStatus.Success);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should call setEmbeddedShellFocused(false) because isThisShellFocused became false
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false);
|
||||||
|
expect(lastFrame()).not.toContain('(Shift+Tab to unfocus)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Snapshots', () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
'renders in Executing state',
|
||||||
|
{ status: ToolCallStatus.Executing },
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'renders in Success state (history mode)',
|
||||||
|
{ status: ToolCallStatus.Success },
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'renders in Error state',
|
||||||
|
{ status: ToolCallStatus.Error, resultDisplay: 'Error output' },
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'renders in Alternate Buffer mode while focused',
|
||||||
|
{
|
||||||
|
status: ToolCallStatus.Executing,
|
||||||
|
embeddedShellFocused: true,
|
||||||
|
activeShellPtyId: 1,
|
||||||
|
ptyId: 1,
|
||||||
|
},
|
||||||
|
{ useAlternateBuffer: true },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'renders in Alternate Buffer mode while unfocused',
|
||||||
|
{
|
||||||
|
status: ToolCallStatus.Executing,
|
||||||
|
embeddedShellFocused: false,
|
||||||
|
activeShellPtyId: 1,
|
||||||
|
ptyId: 1,
|
||||||
|
},
|
||||||
|
{ useAlternateBuffer: true },
|
||||||
|
],
|
||||||
|
])('%s', async (_, props, options) => {
|
||||||
|
const { lastFrame } = renderShell(props, options);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Height Constraints', () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
'respects availableTerminalHeight when it is smaller than ACTIVE_SHELL_MAX_LINES',
|
||||||
|
10,
|
||||||
|
8,
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large',
|
||||||
|
100,
|
||||||
|
ACTIVE_SHELL_MAX_LINES,
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uses full availableTerminalHeight when focused in alternate buffer mode',
|
||||||
|
100,
|
||||||
|
98, // 100 - 2
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'defaults to ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is undefined',
|
||||||
|
undefined,
|
||||||
|
ACTIVE_SHELL_MAX_LINES,
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
])('%s', async (_, availableTerminalHeight, expectedMaxLines, focused) => {
|
||||||
|
const { lastFrame } = renderShell(
|
||||||
|
{
|
||||||
|
resultDisplay: LONG_OUTPUT,
|
||||||
|
renderOutputAsMarkdown: false,
|
||||||
|
availableTerminalHeight,
|
||||||
|
activeShellPtyId: 1,
|
||||||
|
ptyId: focused ? 1 : 2,
|
||||||
|
status: ToolCallStatus.Executing,
|
||||||
|
embeddedShellFocused: focused,
|
||||||
|
},
|
||||||
|
{ useAlternateBuffer: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const frame = lastFrame();
|
||||||
|
expect(frame!.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
|
||||||
|
expect(frame).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ import {
|
|||||||
FocusHint,
|
FocusHint,
|
||||||
} from './ToolShared.js';
|
} from './ToolShared.js';
|
||||||
import type { ToolMessageProps } from './ToolMessage.js';
|
import type { ToolMessageProps } from './ToolMessage.js';
|
||||||
|
import { ToolCallStatus } from '../../types.js';
|
||||||
|
import {
|
||||||
|
ACTIVE_SHELL_MAX_LINES,
|
||||||
|
COMPLETED_SHELL_MAX_LINES,
|
||||||
|
} from '../../constants.js';
|
||||||
|
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export interface ShellToolMessageProps extends ToolMessageProps {
|
export interface ShellToolMessageProps extends ToolMessageProps {
|
||||||
@@ -61,6 +67,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
|
|
||||||
borderDimColor,
|
borderDimColor,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
const isThisShellFocused = checkIsShellFocused(
|
const isThisShellFocused = checkIsShellFocused(
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
@@ -70,6 +77,18 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { setEmbeddedShellFocused } = useUIActions();
|
const { setEmbeddedShellFocused } = useUIActions();
|
||||||
|
const wasFocusedRef = React.useRef(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isThisShellFocused) {
|
||||||
|
wasFocusedRef.current = true;
|
||||||
|
} else if (wasFocusedRef.current) {
|
||||||
|
if (embeddedShellFocused) {
|
||||||
|
setEmbeddedShellFocused(false);
|
||||||
|
}
|
||||||
|
wasFocusedRef.current = false;
|
||||||
|
}
|
||||||
|
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
|
||||||
|
|
||||||
const headerRef = React.useRef<DOMElement>(null);
|
const headerRef = React.useRef<DOMElement>(null);
|
||||||
|
|
||||||
@@ -139,12 +158,20 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
availableTerminalHeight={availableTerminalHeight}
|
availableTerminalHeight={availableTerminalHeight}
|
||||||
terminalWidth={terminalWidth}
|
terminalWidth={terminalWidth}
|
||||||
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
||||||
|
hasFocus={isThisShellFocused}
|
||||||
|
maxLines={getShellMaxLines(
|
||||||
|
status,
|
||||||
|
isAlternateBuffer,
|
||||||
|
isThisShellFocused,
|
||||||
|
availableTerminalHeight,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{isThisShellFocused && config && (
|
{isThisShellFocused && config && (
|
||||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
||||||
<ShellInputPrompt
|
<ShellInputPrompt
|
||||||
activeShellPtyId={activeShellPtyId ?? null}
|
activeShellPtyId={activeShellPtyId ?? null}
|
||||||
focus={embeddedShellFocused}
|
focus={embeddedShellFocused}
|
||||||
|
scrollPageSize={availableTerminalHeight ?? ACTIVE_SHELL_MAX_LINES}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -152,3 +179,39 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the maximum number of lines to display for shell output.
|
||||||
|
*
|
||||||
|
* For completed processes (Success, Error, Canceled), it returns COMPLETED_SHELL_MAX_LINES.
|
||||||
|
* For active processes, it returns the available terminal height if in alternate buffer mode
|
||||||
|
* and focused. Otherwise, it returns ACTIVE_SHELL_MAX_LINES.
|
||||||
|
*
|
||||||
|
* This function ensures a finite number of lines is always returned to prevent performance issues.
|
||||||
|
*/
|
||||||
|
function getShellMaxLines(
|
||||||
|
status: ToolCallStatus,
|
||||||
|
isAlternateBuffer: boolean,
|
||||||
|
isThisShellFocused: boolean,
|
||||||
|
availableTerminalHeight: number | undefined,
|
||||||
|
): number {
|
||||||
|
if (
|
||||||
|
status === ToolCallStatus.Success ||
|
||||||
|
status === ToolCallStatus.Error ||
|
||||||
|
status === ToolCallStatus.Canceled
|
||||||
|
) {
|
||||||
|
return COMPLETED_SHELL_MAX_LINES;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableTerminalHeight === undefined) {
|
||||||
|
return ACTIVE_SHELL_MAX_LINES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLinesBasedOnHeight = Math.max(1, availableTerminalHeight - 2);
|
||||||
|
|
||||||
|
if (isAlternateBuffer && isThisShellFocused) {
|
||||||
|
return maxLinesBasedOnHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(maxLinesBasedOnHeight, ACTIVE_SHELL_MAX_LINES);
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ const isAskUserInProgress = (t: IndividualToolCallDisplay): boolean =>
|
|||||||
].includes(t.status);
|
].includes(t.status);
|
||||||
|
|
||||||
// Main component renders the border and maps the tools using ToolMessage
|
// Main component renders the border and maps the tools using ToolMessage
|
||||||
|
const TOOL_MESSAGE_HORIZONTAL_MARGIN = 4;
|
||||||
|
const TOOL_CONFIRMATION_INTERNAL_PADDING = 4;
|
||||||
|
|
||||||
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||||
toolCalls: allToolCalls,
|
toolCalls: allToolCalls,
|
||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
@@ -142,6 +145,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// This box doesn't have a border even though it conceptually does because
|
// This box doesn't have a border even though it conceptually does because
|
||||||
// we need to allow the sticky headers to render the borders themselves so
|
// we need to allow the sticky headers to render the borders themselves so
|
||||||
@@ -155,6 +160,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
cause tearing.
|
cause tearing.
|
||||||
*/
|
*/
|
||||||
width={terminalWidth}
|
width={terminalWidth}
|
||||||
|
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
||||||
>
|
>
|
||||||
{visibleToolCalls.map((tool, index) => {
|
{visibleToolCalls.map((tool, index) => {
|
||||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||||
@@ -164,7 +170,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
const commonProps = {
|
const commonProps = {
|
||||||
...tool,
|
...tool,
|
||||||
availableTerminalHeight: availableTerminalHeightPerToolMessage,
|
availableTerminalHeight: availableTerminalHeightPerToolMessage,
|
||||||
terminalWidth,
|
terminalWidth: contentWidth,
|
||||||
emphasis: isConfirming
|
emphasis: isConfirming
|
||||||
? ('high' as const)
|
? ('high' as const)
|
||||||
: toolAwaitingApproval
|
: toolAwaitingApproval
|
||||||
@@ -183,7 +189,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
key={tool.callId}
|
key={tool.callId}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
minHeight={1}
|
minHeight={1}
|
||||||
width={terminalWidth}
|
width={contentWidth}
|
||||||
>
|
>
|
||||||
{isShellToolCall ? (
|
{isShellToolCall ? (
|
||||||
<ShellToolMessage
|
<ShellToolMessage
|
||||||
@@ -218,7 +224,9 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
availableTerminalHeight={
|
availableTerminalHeight={
|
||||||
availableTerminalHeightPerToolMessage
|
availableTerminalHeightPerToolMessage
|
||||||
}
|
}
|
||||||
terminalWidth={terminalWidth - 4}
|
terminalWidth={
|
||||||
|
contentWidth - TOOL_CONFIRMATION_INTERNAL_PADDING
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tool.outputFile && (
|
{tool.outputFile && (
|
||||||
@@ -240,7 +248,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
|
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
|
||||||
<Box
|
<Box
|
||||||
height={0}
|
height={0}
|
||||||
width={terminalWidth}
|
width={contentWidth}
|
||||||
borderLeft={true}
|
borderLeft={true}
|
||||||
borderRight={true}
|
borderRight={true}
|
||||||
borderTop={false}
|
borderTop={false}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import type React from 'react';
|
||||||
import type { ToolMessageProps } from './ToolMessage.js';
|
import { ToolMessage, type ToolMessageProps } from './ToolMessage.js';
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { ToolMessage } from './ToolMessage.js';
|
|
||||||
import { StreamingState, ToolCallStatus } from '../../types.js';
|
import { StreamingState, ToolCallStatus } from '../../types.js';
|
||||||
import { Text } from 'ink';
|
import { Text } from 'ink';
|
||||||
import { StreamingContext } from '../../contexts/StreamingContext.js';
|
|
||||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
||||||
@@ -29,45 +27,6 @@ vi.mock('../TerminalOutput.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../AnsiOutput.js', () => ({
|
|
||||||
AnsiOutputText: function MockAnsiOutputText({ data }: { data: AnsiOutput }) {
|
|
||||||
// Simple serialization for snapshot stability
|
|
||||||
const serialized = data
|
|
||||||
.map((line) => line.map((token) => token.text || '').join(''))
|
|
||||||
.join('\n');
|
|
||||||
return <Text>MockAnsiOutput:{serialized}</Text>;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock child components or utilities if they are complex or have side effects
|
|
||||||
vi.mock('../GeminiRespondingSpinner.js', () => ({
|
|
||||||
GeminiRespondingSpinner: ({
|
|
||||||
nonRespondingDisplay,
|
|
||||||
}: {
|
|
||||||
nonRespondingDisplay?: string;
|
|
||||||
}) => {
|
|
||||||
const streamingState = React.useContext(StreamingContext)!;
|
|
||||||
if (streamingState === StreamingState.Responding) {
|
|
||||||
return <Text>MockRespondingSpinner</Text>;
|
|
||||||
}
|
|
||||||
return nonRespondingDisplay ? <Text>{nonRespondingDisplay}</Text> : null;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
vi.mock('./DiffRenderer.js', () => ({
|
|
||||||
DiffRenderer: function MockDiffRenderer({
|
|
||||||
diffContent,
|
|
||||||
}: {
|
|
||||||
diffContent: string;
|
|
||||||
}) {
|
|
||||||
return <Text>MockDiff:{diffContent}</Text>;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
|
||||||
MarkdownDisplay: function MockMarkdownDisplay({ text }: { text: string }) {
|
|
||||||
return <Text>MockMarkdown:{text}</Text>;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('<ToolMessage />', () => {
|
describe('<ToolMessage />', () => {
|
||||||
const baseProps: ToolMessageProps = {
|
const baseProps: ToolMessageProps = {
|
||||||
callId: 'tool-123',
|
callId: 'tool-123',
|
||||||
@@ -131,7 +90,6 @@ describe('<ToolMessage />', () => {
|
|||||||
expect(output).toContain('"a": 1');
|
expect(output).toContain('"a": 1');
|
||||||
expect(output).toContain('"b": [');
|
expect(output).toContain('"b": [');
|
||||||
// Should not use markdown renderer for JSON
|
// Should not use markdown renderer for JSON
|
||||||
expect(output).not.toContain('MockMarkdown:');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders pretty JSON in ink frame', () => {
|
it('renders pretty JSON in ink frame', () => {
|
||||||
@@ -143,9 +101,6 @@ describe('<ToolMessage />', () => {
|
|||||||
const frame = lastFrame();
|
const frame = lastFrame();
|
||||||
|
|
||||||
expect(frame).toMatchSnapshot();
|
expect(frame).toMatchSnapshot();
|
||||||
expect(frame).not.toContain('MockMarkdown:');
|
|
||||||
expect(frame).not.toContain('MockAnsiOutput:');
|
|
||||||
expect(frame).not.toMatch(/MockDiff:/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses JSON renderer even when renderOutputAsMarkdown=true is true', () => {
|
it('uses JSON renderer even when renderOutputAsMarkdown=true is true', () => {
|
||||||
@@ -167,7 +122,6 @@ describe('<ToolMessage />', () => {
|
|||||||
expect(output).toContain('"a": 1');
|
expect(output).toContain('"a": 1');
|
||||||
expect(output).toContain('"b": [');
|
expect(output).toContain('"b": [');
|
||||||
// Should not use markdown renderer for JSON even when renderOutputAsMarkdown=true
|
// Should not use markdown renderer for JSON even when renderOutputAsMarkdown=true
|
||||||
expect(output).not.toContain('MockMarkdown:');
|
|
||||||
});
|
});
|
||||||
it('falls back to plain text for malformed JSON', () => {
|
it('falls back to plain text for malformed JSON', () => {
|
||||||
const testJSONstring = 'a": 1, "b": [2, 3]}';
|
const testJSONstring = 'a": 1, "b": [2, 3]}';
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||||||
availableTerminalHeight={availableTerminalHeight}
|
availableTerminalHeight={availableTerminalHeight}
|
||||||
terminalWidth={terminalWidth}
|
terminalWidth={terminalWidth}
|
||||||
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
||||||
|
hasFocus={isThisShellFocused}
|
||||||
/>
|
/>
|
||||||
{isThisShellFocused && config && (
|
{isThisShellFocused && config && (
|
||||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
||||||
|
|||||||
@@ -4,34 +4,21 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from '../../../test-utils/render.js';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { Box, Text } from 'ink';
|
|
||||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
// Mock child components to simplify testing
|
// Mock UIStateContext partially
|
||||||
vi.mock('./DiffRenderer.js', () => ({
|
|
||||||
DiffRenderer: ({
|
|
||||||
diffContent,
|
|
||||||
filename,
|
|
||||||
}: {
|
|
||||||
diffContent: string;
|
|
||||||
filename: string;
|
|
||||||
}) => (
|
|
||||||
<Box>
|
|
||||||
<Text>
|
|
||||||
DiffRenderer: {filename} - {diffContent}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock UIStateContext
|
|
||||||
const mockUseUIState = vi.fn();
|
const mockUseUIState = vi.fn();
|
||||||
vi.mock('../../contexts/UIStateContext.js', () => ({
|
vi.mock('../../contexts/UIStateContext.js', async (importOriginal) => {
|
||||||
useUIState: () => mockUseUIState(),
|
const actual =
|
||||||
}));
|
await importOriginal<typeof import('../../contexts/UIStateContext.js')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useUIState: () => mockUseUIState(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Mock useAlternateBuffer
|
// Mock useAlternateBuffer
|
||||||
const mockUseAlternateBuffer = vi.fn();
|
const mockUseAlternateBuffer = vi.fn();
|
||||||
@@ -39,28 +26,6 @@ vi.mock('../../hooks/useAlternateBuffer.js', () => ({
|
|||||||
useAlternateBuffer: () => mockUseAlternateBuffer(),
|
useAlternateBuffer: () => mockUseAlternateBuffer(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock useSettings
|
|
||||||
vi.mock('../../contexts/SettingsContext.js', () => ({
|
|
||||||
useSettings: () => ({
|
|
||||||
merged: {
|
|
||||||
ui: {
|
|
||||||
useAlternateBuffer: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock useOverflowActions
|
|
||||||
vi.mock('../../contexts/OverflowContext.js', () => ({
|
|
||||||
useOverflowActions: () => ({
|
|
||||||
addOverflowingId: vi.fn(),
|
|
||||||
removeOverflowingId: vi.fn(),
|
|
||||||
}),
|
|
||||||
useOverflowState: () => ({
|
|
||||||
overflowingIds: new Set(),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ToolResultDisplay', () => {
|
describe('ToolResultDisplay', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -68,6 +33,66 @@ describe('ToolResultDisplay', () => {
|
|||||||
mockUseAlternateBuffer.mockReturnValue(false);
|
mockUseAlternateBuffer.mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper to use renderWithProviders
|
||||||
|
const render = (ui: React.ReactElement) => renderWithProviders(ui);
|
||||||
|
|
||||||
|
it('uses ScrollableList for ANSI output in alternate buffer mode', () => {
|
||||||
|
mockUseAlternateBuffer.mockReturnValue(true);
|
||||||
|
const content = 'ansi content';
|
||||||
|
const ansiResult: AnsiOutput = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: content,
|
||||||
|
fg: 'red',
|
||||||
|
bg: 'black',
|
||||||
|
bold: false,
|
||||||
|
italic: false,
|
||||||
|
underline: false,
|
||||||
|
dim: false,
|
||||||
|
inverse: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<ToolResultDisplay
|
||||||
|
resultDisplay={ansiResult}
|
||||||
|
terminalWidth={80}
|
||||||
|
maxLines={10}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const output = lastFrame();
|
||||||
|
|
||||||
|
expect(output).toContain(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses Scrollable for non-ANSI output in alternate buffer mode', () => {
|
||||||
|
mockUseAlternateBuffer.mockReturnValue(true);
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<ToolResultDisplay
|
||||||
|
resultDisplay="**Markdown content**"
|
||||||
|
terminalWidth={80}
|
||||||
|
maxLines={10}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const output = lastFrame();
|
||||||
|
|
||||||
|
// With real components, we check for the content itself
|
||||||
|
expect(output).toContain('Markdown content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes hasFocus prop to scrollable components', () => {
|
||||||
|
mockUseAlternateBuffer.mockReturnValue(true);
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<ToolResultDisplay
|
||||||
|
resultDisplay="Some result"
|
||||||
|
terminalWidth={80}
|
||||||
|
hasFocus={true}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('Some result');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders string result as markdown by default', () => {
|
it('renders string result as markdown by default', () => {
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<ToolResultDisplay resultDisplay="**Some result**" terminalWidth={80} />,
|
<ToolResultDisplay resultDisplay="**Some result**" terminalWidth={80} />,
|
||||||
@@ -194,4 +219,86 @@ describe('ToolResultDisplay', () => {
|
|||||||
|
|
||||||
expect(output).toMatchSnapshot();
|
expect(output).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('truncates ANSI output when maxLines is provided', () => {
|
||||||
|
const ansiResult: AnsiOutput = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Line 1',
|
||||||
|
fg: '',
|
||||||
|
bg: '',
|
||||||
|
bold: false,
|
||||||
|
italic: false,
|
||||||
|
underline: false,
|
||||||
|
dim: false,
|
||||||
|
inverse: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Line 2',
|
||||||
|
fg: '',
|
||||||
|
bg: '',
|
||||||
|
bold: false,
|
||||||
|
italic: false,
|
||||||
|
underline: false,
|
||||||
|
dim: false,
|
||||||
|
inverse: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Line 3',
|
||||||
|
fg: '',
|
||||||
|
bg: '',
|
||||||
|
bold: false,
|
||||||
|
italic: false,
|
||||||
|
underline: false,
|
||||||
|
dim: false,
|
||||||
|
inverse: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<ToolResultDisplay
|
||||||
|
resultDisplay={ansiResult}
|
||||||
|
terminalWidth={80}
|
||||||
|
availableTerminalHeight={20}
|
||||||
|
maxLines={2}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const output = lastFrame();
|
||||||
|
|
||||||
|
expect(output).not.toContain('Line 1');
|
||||||
|
expect(output).toContain('Line 2');
|
||||||
|
expect(output).toContain('Line 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates ANSI output when maxLines is provided, even if availableTerminalHeight is undefined', () => {
|
||||||
|
const ansiResult: AnsiOutput = Array.from({ length: 50 }, (_, i) => [
|
||||||
|
{
|
||||||
|
text: `Line ${i + 1}`,
|
||||||
|
fg: '',
|
||||||
|
bg: '',
|
||||||
|
bold: false,
|
||||||
|
italic: false,
|
||||||
|
underline: false,
|
||||||
|
dim: false,
|
||||||
|
inverse: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<ToolResultDisplay
|
||||||
|
resultDisplay={ansiResult}
|
||||||
|
terminalWidth={80}
|
||||||
|
maxLines={25}
|
||||||
|
availableTerminalHeight={undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const output = lastFrame();
|
||||||
|
|
||||||
|
// It SHOULD truncate to 25 lines because maxLines is provided
|
||||||
|
expect(output).not.toContain('Line 1');
|
||||||
|
expect(output).toContain('Line 50');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ import React from 'react';
|
|||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { DiffRenderer } from './DiffRenderer.js';
|
import { DiffRenderer } from './DiffRenderer.js';
|
||||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||||
import { AnsiOutputText } from '../AnsiOutput.js';
|
import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js';
|
||||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
import type { AnsiOutput, AnsiLine } from '@google/gemini-cli-core';
|
||||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||||
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
||||||
|
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||||
|
import { Scrollable } from '../shared/Scrollable.js';
|
||||||
|
import { ScrollableList } from '../shared/ScrollableList.js';
|
||||||
|
import { SCROLL_TO_ITEM_END } from '../shared/VirtualizedList.js';
|
||||||
|
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
|
||||||
|
|
||||||
const STATIC_HEIGHT = 1;
|
const STATIC_HEIGHT = 1;
|
||||||
const RESERVED_LINE_COUNT = 6; // for tool name, status, padding, and 'ShowMoreLines' hint
|
const RESERVED_LINE_COUNT = 6; // for tool name, status, padding, and 'ShowMoreLines' hint
|
||||||
@@ -28,6 +33,8 @@ export interface ToolResultDisplayProps {
|
|||||||
availableTerminalHeight?: number;
|
availableTerminalHeight?: number;
|
||||||
terminalWidth: number;
|
terminalWidth: number;
|
||||||
renderOutputAsMarkdown?: boolean;
|
renderOutputAsMarkdown?: boolean;
|
||||||
|
maxLines?: number;
|
||||||
|
hasFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileDiffResult {
|
interface FileDiffResult {
|
||||||
@@ -40,30 +47,100 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
|||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
renderOutputAsMarkdown = true,
|
renderOutputAsMarkdown = true,
|
||||||
|
maxLines,
|
||||||
|
hasFocus = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { renderMarkdown } = useUIState();
|
const { renderMarkdown } = useUIState();
|
||||||
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
|
|
||||||
const availableHeight = availableTerminalHeight
|
let availableHeight = availableTerminalHeight
|
||||||
? Math.max(
|
? Math.max(
|
||||||
availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT,
|
availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT,
|
||||||
MIN_LINES_SHOWN + 1, // enforce minimum lines shown
|
MIN_LINES_SHOWN + 1, // enforce minimum lines shown
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
if (maxLines && availableHeight) {
|
||||||
|
availableHeight = Math.min(availableHeight, maxLines);
|
||||||
|
}
|
||||||
|
|
||||||
const combinedPaddingAndBorderWidth = 4;
|
const combinedPaddingAndBorderWidth = 4;
|
||||||
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
|
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
|
||||||
|
|
||||||
|
const keyExtractor = React.useCallback(
|
||||||
|
(_: AnsiLine, index: number) => index.toString(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderVirtualizedAnsiLine = React.useCallback(
|
||||||
|
({ item }: { item: AnsiLine }) => (
|
||||||
|
<Box height={1} overflow="hidden">
|
||||||
|
<AnsiLineText line={item} />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const truncatedResultDisplay = React.useMemo(() => {
|
const truncatedResultDisplay = React.useMemo(() => {
|
||||||
if (typeof resultDisplay === 'string') {
|
// Only truncate string output if not in alternate buffer mode to ensure
|
||||||
if (resultDisplay.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
// we can scroll through the full output.
|
||||||
return '...' + resultDisplay.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
if (typeof resultDisplay === 'string' && !isAlternateBuffer) {
|
||||||
|
let text = resultDisplay;
|
||||||
|
if (text.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||||
|
text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||||
}
|
}
|
||||||
|
if (maxLines) {
|
||||||
|
const hasTrailingNewline = text.endsWith('\n');
|
||||||
|
const contentText = hasTrailingNewline ? text.slice(0, -1) : text;
|
||||||
|
const lines = contentText.split('\n');
|
||||||
|
if (lines.length > maxLines) {
|
||||||
|
text =
|
||||||
|
lines.slice(-maxLines).join('\n') +
|
||||||
|
(hasTrailingNewline ? '\n' : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
return resultDisplay;
|
return resultDisplay;
|
||||||
}, [resultDisplay]);
|
}, [resultDisplay, isAlternateBuffer, maxLines]);
|
||||||
|
|
||||||
if (!truncatedResultDisplay) return null;
|
if (!truncatedResultDisplay) return null;
|
||||||
|
|
||||||
|
// 1. Early return for background tools (Todos)
|
||||||
|
if (
|
||||||
|
typeof truncatedResultDisplay === 'object' &&
|
||||||
|
'todos' in truncatedResultDisplay
|
||||||
|
) {
|
||||||
|
// display nothing, as the TodoTray will handle rendering todos
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. High-performance path: Virtualized ANSI in interactive mode
|
||||||
|
if (isAlternateBuffer && Array.isArray(truncatedResultDisplay)) {
|
||||||
|
// If availableHeight is undefined, fallback to a safe default to prevents infinite loop
|
||||||
|
// where Container grows -> List renders more -> Container grows.
|
||||||
|
const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES;
|
||||||
|
const listHeight = Math.min(
|
||||||
|
(truncatedResultDisplay as AnsiOutput).length,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
|
||||||
|
<ScrollableList
|
||||||
|
width={childWidth}
|
||||||
|
data={truncatedResultDisplay as AnsiOutput}
|
||||||
|
renderItem={renderVirtualizedAnsiLine}
|
||||||
|
estimatedItemHeight={() => 1}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
initialScrollIndex={SCROLL_TO_ITEM_END}
|
||||||
|
hasFocus={hasFocus}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Compute content node for non-virtualized paths
|
||||||
// Check if string content is valid JSON and pretty-print it
|
// Check if string content is valid JSON and pretty-print it
|
||||||
const prettyJSON =
|
const prettyJSON =
|
||||||
typeof truncatedResultDisplay === 'string'
|
typeof truncatedResultDisplay === 'string'
|
||||||
@@ -113,22 +190,38 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
|||||||
terminalWidth={childWidth}
|
terminalWidth={childWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (
|
|
||||||
typeof truncatedResultDisplay === 'object' &&
|
|
||||||
'todos' in truncatedResultDisplay
|
|
||||||
) {
|
|
||||||
// display nothing, as the TodoTray will handle rendering todos
|
|
||||||
return null;
|
|
||||||
} else {
|
} else {
|
||||||
|
const shouldDisableTruncation =
|
||||||
|
isAlternateBuffer ||
|
||||||
|
(availableTerminalHeight === undefined && maxLines === undefined);
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
<AnsiOutputText
|
<AnsiOutputText
|
||||||
data={truncatedResultDisplay as AnsiOutput}
|
data={truncatedResultDisplay as AnsiOutput}
|
||||||
availableTerminalHeight={availableHeight}
|
availableTerminalHeight={
|
||||||
|
isAlternateBuffer ? undefined : availableHeight
|
||||||
|
}
|
||||||
width={childWidth}
|
width={childWidth}
|
||||||
|
maxLines={isAlternateBuffer ? undefined : maxLines}
|
||||||
|
disableTruncation={shouldDisableTruncation}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Final render based on session mode
|
||||||
|
if (isAlternateBuffer) {
|
||||||
|
return (
|
||||||
|
<Scrollable
|
||||||
|
width={childWidth}
|
||||||
|
maxHeight={maxLines ?? availableHeight}
|
||||||
|
hasFocus={hasFocus} // Allow scrolling via keyboard (Shift+Up/Down)
|
||||||
|
scrollToBottom={true}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Scrollable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box width={childWidth} flexDirection="column">
|
<Box width={childWidth} flexDirection="column">
|
||||||
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
|
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ describe('ToolResultDisplay Overflow', () => {
|
|||||||
streamingState: StreamingState.Idle,
|
streamingState: StreamingState.Idle,
|
||||||
constrainHeight: true,
|
constrainHeight: true,
|
||||||
},
|
},
|
||||||
|
useAlternateBuffer: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Height Constraints > defaults to ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is undefined 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command A shell command │
|
||||||
|
│ │
|
||||||
|
│ Line 86 │
|
||||||
|
│ Line 87 │
|
||||||
|
│ Line 88 │
|
||||||
|
│ Line 89 │
|
||||||
|
│ Line 90 │
|
||||||
|
│ Line 91 │
|
||||||
|
│ Line 92 │
|
||||||
|
│ Line 93 │
|
||||||
|
│ Line 94 │
|
||||||
|
│ Line 95 │
|
||||||
|
│ Line 96 │
|
||||||
|
│ Line 97 │
|
||||||
|
│ Line 98 ▄ │
|
||||||
|
│ Line 99 █ │
|
||||||
|
│ Line 100 █ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Height Constraints > respects availableTerminalHeight when it is smaller than ACTIVE_SHELL_MAX_LINES 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command A shell command │
|
||||||
|
│ │
|
||||||
|
│ Line 93 │
|
||||||
|
│ Line 94 │
|
||||||
|
│ Line 95 │
|
||||||
|
│ Line 96 │
|
||||||
|
│ Line 97 │
|
||||||
|
│ Line 98 │
|
||||||
|
│ Line 99 │
|
||||||
|
│ Line 100 █ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Height Constraints > uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command A shell command │
|
||||||
|
│ │
|
||||||
|
│ Line 86 │
|
||||||
|
│ Line 87 │
|
||||||
|
│ Line 88 │
|
||||||
|
│ Line 89 │
|
||||||
|
│ Line 90 │
|
||||||
|
│ Line 91 │
|
||||||
|
│ Line 92 │
|
||||||
|
│ Line 93 │
|
||||||
|
│ Line 94 │
|
||||||
|
│ Line 95 │
|
||||||
|
│ Line 96 │
|
||||||
|
│ Line 97 │
|
||||||
|
│ Line 98 ▄ │
|
||||||
|
│ Line 99 █ │
|
||||||
|
│ Line 100 █ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Height Constraints > uses full availableTerminalHeight when focused in alternate buffer mode 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command A shell command (Shift+Tab to unfocus) │
|
||||||
|
│ │
|
||||||
|
│ Line 3 │
|
||||||
|
│ Line 4 │
|
||||||
|
│ Line 5 █ │
|
||||||
|
│ Line 6 █ │
|
||||||
|
│ Line 7 █ │
|
||||||
|
│ Line 8 █ │
|
||||||
|
│ Line 9 █ │
|
||||||
|
│ Line 10 █ │
|
||||||
|
│ Line 11 █ │
|
||||||
|
│ Line 12 █ │
|
||||||
|
│ Line 13 █ │
|
||||||
|
│ Line 14 █ │
|
||||||
|
│ Line 15 █ │
|
||||||
|
│ Line 16 █ │
|
||||||
|
│ Line 17 █ │
|
||||||
|
│ Line 18 █ │
|
||||||
|
│ Line 19 █ │
|
||||||
|
│ Line 20 █ │
|
||||||
|
│ Line 21 █ │
|
||||||
|
│ Line 22 █ │
|
||||||
|
│ Line 23 █ │
|
||||||
|
│ Line 24 █ │
|
||||||
|
│ Line 25 █ │
|
||||||
|
│ Line 26 █ │
|
||||||
|
│ Line 27 █ │
|
||||||
|
│ Line 28 █ │
|
||||||
|
│ Line 29 █ │
|
||||||
|
│ Line 30 █ │
|
||||||
|
│ Line 31 █ │
|
||||||
|
│ Line 32 █ │
|
||||||
|
│ Line 33 █ │
|
||||||
|
│ Line 34 █ │
|
||||||
|
│ Line 35 █ │
|
||||||
|
│ Line 36 █ │
|
||||||
|
│ Line 37 █ │
|
||||||
|
│ Line 38 █ │
|
||||||
|
│ Line 39 █ │
|
||||||
|
│ Line 40 █ │
|
||||||
|
│ Line 41 █ │
|
||||||
|
│ Line 42 █ │
|
||||||
|
│ Line 43 █ │
|
||||||
|
│ Line 44 █ │
|
||||||
|
│ Line 45 █ │
|
||||||
|
│ Line 46 █ │
|
||||||
|
│ Line 47 █ │
|
||||||
|
│ Line 48 █ │
|
||||||
|
│ Line 49 █ │
|
||||||
|
│ Line 50 █ │
|
||||||
|
│ Line 51 █ │
|
||||||
|
│ Line 52 █ │
|
||||||
|
│ Line 53 █ │
|
||||||
|
│ Line 54 █ │
|
||||||
|
│ Line 55 █ │
|
||||||
|
│ Line 56 █ │
|
||||||
|
│ Line 57 █ │
|
||||||
|
│ Line 58 █ │
|
||||||
|
│ Line 59 █ │
|
||||||
|
│ Line 60 █ │
|
||||||
|
│ Line 61 █ │
|
||||||
|
│ Line 62 █ │
|
||||||
|
│ Line 63 █ │
|
||||||
|
│ Line 64 █ │
|
||||||
|
│ Line 65 █ │
|
||||||
|
│ Line 66 █ │
|
||||||
|
│ Line 67 █ │
|
||||||
|
│ Line 68 █ │
|
||||||
|
│ Line 69 █ │
|
||||||
|
│ Line 70 █ │
|
||||||
|
│ Line 71 █ │
|
||||||
|
│ Line 72 █ │
|
||||||
|
│ Line 73 █ │
|
||||||
|
│ Line 74 █ │
|
||||||
|
│ Line 75 █ │
|
||||||
|
│ Line 76 █ │
|
||||||
|
│ Line 77 █ │
|
||||||
|
│ Line 78 █ │
|
||||||
|
│ Line 79 █ │
|
||||||
|
│ Line 80 █ │
|
||||||
|
│ Line 81 █ │
|
||||||
|
│ Line 82 █ │
|
||||||
|
│ Line 83 █ │
|
||||||
|
│ Line 84 █ │
|
||||||
|
│ Line 85 █ │
|
||||||
|
│ Line 86 █ │
|
||||||
|
│ Line 87 █ │
|
||||||
|
│ Line 88 █ │
|
||||||
|
│ Line 89 █ │
|
||||||
|
│ Line 90 █ │
|
||||||
|
│ Line 91 █ │
|
||||||
|
│ Line 92 █ │
|
||||||
|
│ Line 93 █ │
|
||||||
|
│ Line 94 █ │
|
||||||
|
│ Line 95 █ │
|
||||||
|
│ Line 96 █ │
|
||||||
|
│ Line 97 █ │
|
||||||
|
│ Line 98 █ │
|
||||||
|
│ Line 99 █ │
|
||||||
|
│ Line 100 █ │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while focused 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command A shell command (Shift+Tab to unfocus) │
|
||||||
|
│ │
|
||||||
|
│ Test result │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while unfocused 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command A shell command │
|
||||||
|
│ │
|
||||||
|
│ Test result │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Snapshots > renders in Error state 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ x Shell Command A shell command │
|
||||||
|
│ │
|
||||||
|
│ Error output │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Snapshots > renders in Executing state 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ⊷ Shell Command A shell command │
|
||||||
|
│ │
|
||||||
|
│ Test result │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ShellToolMessage /> > Snapshots > renders in Success state (history mode) 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ✓ Shell Command A shell command │
|
||||||
|
│ │
|
||||||
|
│ Test result │"
|
||||||
|
`;
|
||||||
+13
-13
@@ -1,18 +1,18 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`ToolConfirmationMessage Overflow > should display "press ctrl-o" hint when content overflows in ToolGroupMessage 1`] = `
|
exports[`ToolConfirmationMessage Overflow > should display "press ctrl-o" hint when content overflows in ToolGroupMessage 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ? test-tool a test tool ← │
|
│ ? test-tool a test tool ← │
|
||||||
│ │
|
│ │
|
||||||
│ ... first 49 lines hidden ... │
|
│ ... first 49 lines hidden ... │
|
||||||
│ 50 line 50 │
|
│ 50 line 50 │
|
||||||
│ Apply this change? │
|
│ Apply this change? │
|
||||||
│ │
|
│ │
|
||||||
│ ● 1. Allow once │
|
│ ● 1. Allow once │
|
||||||
│ 2. Allow for this session │
|
│ 2. Allow for this session │
|
||||||
│ 3. Modify with external editor │
|
│ 3. Modify with external editor │
|
||||||
│ 4. No, suggest changes (esc) │
|
│ 4. No, suggest changes (esc) │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────╯
|
||||||
Press ctrl-o to show more lines"
|
Press ctrl-o to show more lines"
|
||||||
`;
|
`;
|
||||||
|
|||||||
+181
-181
@@ -1,19 +1,19 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = `
|
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ x Ask User │
|
│ x Ask User │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = `
|
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ Ask User │
|
│ ✓ Ask User │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`;
|
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`;
|
||||||
@@ -23,89 +23,89 @@ exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when s
|
|||||||
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`;
|
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = `
|
exports[`<ToolGroupMessage /> > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ other-tool A tool for testing │
|
│ ✓ other-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
|
exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ │
|
│ │
|
||||||
│ ✓ another-tool A tool for testing │
|
│ ✓ another-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shell commands even when successful 1`] = `
|
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shell commands even when successful 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ run_shell_command A tool for testing │
|
│ ✓ run_shell_command A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `
|
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ o test-tool A tool for testing │
|
│ o test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = `
|
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ? confirm-tool A tool for testing ← │
|
│ ? confirm-tool A tool for testing ← │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ Do you want to proceed? │
|
│ Do you want to proceed? │
|
||||||
│ Do you want to proceed? │
|
│ Do you want to proceed? │
|
||||||
│ │
|
│ │
|
||||||
│ ● 1. Allow once │
|
│ ● 1. Allow once │
|
||||||
│ 2. Allow for this session │
|
│ 2. Allow for this session │
|
||||||
│ 3. No, suggest changes (esc) │
|
│ 3. No, suggest changes (esc) │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval enabled 1`] = `
|
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval enabled 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ? confirm-tool A tool for testing ← │
|
│ ? confirm-tool A tool for testing ← │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ Do you want to proceed? │
|
│ Do you want to proceed? │
|
||||||
│ Do you want to proceed? │
|
│ Do you want to proceed? │
|
||||||
│ │
|
│ │
|
||||||
│ ● 1. Allow once │
|
│ ● 1. Allow once │
|
||||||
│ 2. Allow for this session │
|
│ 2. Allow for this session │
|
||||||
│ 3. Allow for all future sessions │
|
│ 3. Allow for all future sessions │
|
||||||
│ 4. No, suggest changes (esc) │
|
│ 4. No, suggest changes (esc) │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = `
|
exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ? first-confirm A tool for testing ← │
|
│ ? first-confirm A tool for testing ← │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ Confirm first tool │
|
│ Confirm first tool │
|
||||||
│ Do you want to proceed? │
|
│ Do you want to proceed? │
|
||||||
│ │
|
│ │
|
||||||
│ ● 1. Allow once │
|
│ ● 1. Allow once │
|
||||||
│ 2. Allow for this session │
|
│ 2. Allow for this session │
|
||||||
│ 3. No, suggest changes (esc) │
|
│ 3. No, suggest changes (esc) │
|
||||||
│ │
|
│ │
|
||||||
│ │
|
│ │
|
||||||
│ ? second-confirm A tool for testing │
|
│ ? second-confirm A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`;
|
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`;
|
||||||
@@ -113,148 +113,148 @@ exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools
|
|||||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > renders nothing when only tool is in-progress AskUser with borderBottom=false 1`] = `""`;
|
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > renders nothing when only tool is in-progress AskUser with borderBottom=false 1`] = `""`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > shows only successful tools when mixed with confirming tools 1`] = `
|
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > shows only successful tools when mixed with confirming tools 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ success-tool A tool for testing │
|
│ ✓ success-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool-1 Description 1. This is a long description that will need to be tr… │
|
│ ✓ tool-1 Description 1. This is a long description that will need to b… │
|
||||||
│──────────────────────────────────────────────────────────────────────────────│
|
│──────────────────────────────────────────────────────────────────────────│
|
||||||
│ line5 │ █
|
│ line5 │ █
|
||||||
│ │ █
|
│ │ █
|
||||||
│ ✓ tool-2 Description 2 │ █
|
│ ✓ tool-2 Description 2 │ █
|
||||||
│ │ █
|
│ │ █
|
||||||
│ line1 │ █
|
│ line1 │ █
|
||||||
│ line2 │ █
|
│ line2 │ █
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯ █"
|
╰──────────────────────────────────────────────────────────────────────────╯ █"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including shell command 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including shell command 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ read_file Read a file │
|
│ ✓ read_file Read a file │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ │
|
│ │
|
||||||
│ ⊷ run_shell_command Run command │
|
│ ⊷ run_shell_command Run command │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ │
|
│ │
|
||||||
│ o write_file Write to file │
|
│ o write_file Write to file │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ successful-tool This tool succeeded │
|
│ ✓ successful-tool This tool succeeded │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ │
|
│ │
|
||||||
│ o pending-tool This tool is pending │
|
│ o pending-tool This tool is pending │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ │
|
│ │
|
||||||
│ x error-tool This tool failed │
|
│ x error-tool This tool failed │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with yellow border 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with yellow border 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ run_shell_command Execute shell command │
|
│ ✓ run_shell_command Execute shell command │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful tool call 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful tool call 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting confirmation 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting confirmation 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ? confirmation-tool This tool needs confirmation ← │
|
│ ? confirmation-tool This tool needs confirmation ← │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ Are you sure you want to proceed? │
|
│ Are you sure you want to proceed? │
|
||||||
│ Do you want to proceed? │
|
│ Do you want to proceed? │
|
||||||
│ │
|
│ │
|
||||||
│ ● 1. Allow once │
|
│ ● 1. Allow once │
|
||||||
│ 2. Allow for this session │
|
│ 2. Allow for this session │
|
||||||
│ 3. No, suggest changes (esc) │
|
│ 3. No, suggest changes (esc) │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call with outputFile 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call with outputFile 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool-with-file Tool that saved output to file │
|
│ ✓ tool-with-file Tool that saved output to file │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
│ Output too long and was saved to: /path/to/output.txt │
|
│ Output too long and was saved to: /path/to/output.txt │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where only the last line of the previous group is visible 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where only the last line of the previous group is visible 1`] = `
|
||||||
"╰──────────────────────────────────────────────────────────────────────────────╯
|
"╰──────────────────────────────────────────────────────────────────────────╯
|
||||||
╭──────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool-2 Description 2 │
|
│ ✓ tool-2 Description 2 │
|
||||||
│ │ ▄
|
│ │ ▄
|
||||||
│ line1 │ █
|
│ line1 │ █
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯ █"
|
╰──────────────────────────────────────────────────────────────────────────╯ █"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool-with-result Tool with output │
|
│ ✓ tool-with-result Tool with output │
|
||||||
│ │
|
│ │
|
||||||
│ This is a long result that might need height constraints │
|
│ This is a long result that might need height constraints │
|
||||||
│ │
|
│ │
|
||||||
│ ✓ another-tool Another tool │
|
│ ✓ another-tool Another tool │
|
||||||
│ │
|
│ │
|
||||||
│ More output here │
|
│ More output here │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = `
|
||||||
"╭──────────────────────────────────────╮
|
"╭──────────────────────────────────╮
|
||||||
│ ✓ very-long-tool-name-that-might-w… │
|
│ ✓ very-long-tool-name-that-mig… │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
╰──────────────────────────────────────╯"
|
╰──────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Height Calculation > calculates available height correctly with multiple tools with results 1`] = `
|
exports[`<ToolGroupMessage /> > Height Calculation > calculates available height correctly with multiple tools with results 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Result 1 │
|
│ Result 1 │
|
||||||
│ │
|
│ │
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ Result 2 │
|
│ Result 2 │
|
||||||
│ │
|
│ │
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -14,93 +14,90 @@ exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ? for Confirmin
|
|||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ? test-tool A tool for testing │
|
│ ? test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows - for Canceled status 1`] = `
|
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows - for Canceled status 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ - test-tool A tool for testing │
|
│ - test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = `
|
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ MockRespondingSpinnertest-tool A tool for testing │
|
│ ⊶ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows o for Pending status 1`] = `
|
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows o for Pending status 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ o test-tool A tool for testing │
|
│ o test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = `
|
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ⊷ test-tool A tool for testing │
|
│ ⊷ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = `
|
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ⊷ test-tool A tool for testing │
|
│ ⊷ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows x for Error status 1`] = `
|
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows x for Error status 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ x test-tool A tool for testing │
|
│ x test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ✓ for Success status 1`] = `
|
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows ✓ for Success status 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > renders AnsiOutputText for AnsiOutput results 1`] = `
|
exports[`<ToolMessage /> > renders AnsiOutputText for AnsiOutput results 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockAnsiOutput:hello │"
|
│ hello │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
|
exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockDiff:--- a/file.txt │
|
│ 1 - old │
|
||||||
│ +++ b/file.txt │
|
│ 1 + new │"
|
||||||
│ @@ -1 +1 @@ │
|
|
||||||
│ -old │
|
|
||||||
│ +new │"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > renders basic tool information 1`] = `
|
exports[`<ToolMessage /> > renders basic tool information 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > renders emphasis correctly 1`] = `
|
exports[`<ToolMessage /> > renders emphasis correctly 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing ← │
|
│ ✓ test-tool A tool for testing ← │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolMessage /> > renders emphasis correctly 2`] = `
|
exports[`<ToolMessage /> > renders emphasis correctly 2`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool A tool for testing │
|
│ ✓ test-tool A tool for testing │
|
||||||
│ │
|
│ │
|
||||||
│ MockMarkdown:Test result │"
|
│ Test result │"
|
||||||
`;
|
`;
|
||||||
|
|||||||
+7
-1
@@ -6,7 +6,13 @@ exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with ava
|
|||||||
|
|
||||||
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"ansi content"`;
|
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"ansi content"`;
|
||||||
|
|
||||||
exports[`ToolResultDisplay > renders file diff result 1`] = `"DiffRenderer: test.ts - diff content"`;
|
exports[`ToolResultDisplay > renders file diff result 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ │
|
||||||
|
│ No changes detected. │
|
||||||
|
│ │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────╯"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
|
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -1,14 +1,14 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`ToolResultDisplay Overflow > should display "press ctrl-o" hint when content overflows in ToolGroupMessage 1`] = `
|
exports[`ToolResultDisplay Overflow > should display "press ctrl-o" hint when content overflows in ToolGroupMessage 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ test-tool a test tool │
|
│ ✓ test-tool a test tool │
|
||||||
│ │
|
│ │
|
||||||
│ ... first 46 lines hidden ... │
|
│ ... first 46 lines hidden ... │
|
||||||
│ line 47 │
|
│ line 47 │
|
||||||
│ line 48 │
|
│ line 48 │
|
||||||
│ line 49 │
|
│ line 49 │
|
||||||
│ line 50 │
|
│ line 50 │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────╯
|
||||||
Press ctrl-o to show more lines"
|
Press ctrl-o to show more lines"
|
||||||
`;
|
`;
|
||||||
|
|||||||
+25
-25
@@ -1,41 +1,41 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 1`] = `
|
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 1`] = `
|
||||||
"╭────────────────────────────────────────────────────────────────────────────╮ █
|
"╭────────────────────────────────────────────────────────────────────────╮ █
|
||||||
│ ✓ Shell Command Description for Shell Command │ █
|
│ ✓ Shell Command Description for Shell Command │ █
|
||||||
│ │
|
│ │
|
||||||
│ shell-01 │
|
│ shell-01 │
|
||||||
│ shell-02 │"
|
│ shell-02 │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 2`] = `
|
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 2`] = `
|
||||||
"╭────────────────────────────────────────────────────────────────────────────╮
|
"╭────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ Shell Command Description for Shell Command │ ▄
|
│ ✓ Shell Command Description for Shell Command │ ▄
|
||||||
│────────────────────────────────────────────────────────────────────────────│ █
|
│────────────────────────────────────────────────────────────────────────│ █
|
||||||
│ shell-06 │ ▀
|
│ shell-06 │ ▀
|
||||||
│ shell-07 │"
|
│ shell-07 │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 1`] = `
|
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 1`] = `
|
||||||
"╭────────────────────────────────────────────────────────────────────────────╮ █
|
"╭────────────────────────────────────────────────────────────────────────╮ █
|
||||||
│ ✓ tool-1 Description for tool-1 │
|
│ ✓ tool-1 Description for tool-1 │
|
||||||
│ │
|
│ │
|
||||||
│ c1-01 │
|
│ c1-01 │
|
||||||
│ c1-02 │"
|
│ c1-02 │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 2`] = `
|
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 2`] = `
|
||||||
"╭────────────────────────────────────────────────────────────────────────────╮
|
"╭────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool-1 Description for tool-1 │ █
|
│ ✓ tool-1 Description for tool-1 │ █
|
||||||
│────────────────────────────────────────────────────────────────────────────│
|
│────────────────────────────────────────────────────────────────────────│
|
||||||
│ c1-06 │
|
│ c1-06 │
|
||||||
│ c1-07 │"
|
│ c1-07 │"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 3`] = `
|
exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 3`] = `
|
||||||
"│ │
|
"│ │
|
||||||
│ ✓ tool-2 Description for tool-2 │
|
│ ✓ tool-2 Description for tool-2 │
|
||||||
│────────────────────────────────────────────────────────────────────────────│
|
│────────────────────────────────────────────────────────────────────────│
|
||||||
│ c2-10 │
|
│ c2-10 │
|
||||||
╰────────────────────────────────────────────────────────────────────────────╯ █"
|
╰────────────────────────────────────────────────────────────────────────╯ █"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -117,4 +117,91 @@ describe('<Scrollable />', () => {
|
|||||||
});
|
});
|
||||||
expect(capturedEntry.getScrollState().scrollTop).toBe(1);
|
expect(capturedEntry.getScrollState().scrollTop).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('keypress handling', () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: 'scrolls down when overflow exists and not at bottom',
|
||||||
|
initialScrollTop: 0,
|
||||||
|
scrollHeight: 10,
|
||||||
|
keySequence: '\u001B[1;2B', // Shift+Down
|
||||||
|
expectedScrollTop: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'scrolls up when overflow exists and not at top',
|
||||||
|
initialScrollTop: 2,
|
||||||
|
scrollHeight: 10,
|
||||||
|
keySequence: '\u001B[1;2A', // Shift+Up
|
||||||
|
expectedScrollTop: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not scroll up when at top (allows event to bubble)',
|
||||||
|
initialScrollTop: 0,
|
||||||
|
scrollHeight: 10,
|
||||||
|
keySequence: '\u001B[1;2A', // Shift+Up
|
||||||
|
expectedScrollTop: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not scroll down when at bottom (allows event to bubble)',
|
||||||
|
initialScrollTop: 5, // maxScroll = 10 - 5 = 5
|
||||||
|
scrollHeight: 10,
|
||||||
|
keySequence: '\u001B[1;2B', // Shift+Down
|
||||||
|
expectedScrollTop: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'does not scroll when content fits (allows event to bubble)',
|
||||||
|
initialScrollTop: 0,
|
||||||
|
scrollHeight: 5, // Same as innerHeight (5)
|
||||||
|
keySequence: '\u001B[1;2B', // Shift+Down
|
||||||
|
expectedScrollTop: 0,
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'$name',
|
||||||
|
async ({
|
||||||
|
initialScrollTop,
|
||||||
|
scrollHeight,
|
||||||
|
keySequence,
|
||||||
|
expectedScrollTop,
|
||||||
|
}) => {
|
||||||
|
// Dynamically import ink to mock getScrollHeight
|
||||||
|
const ink = await import('ink');
|
||||||
|
vi.mocked(ink.getScrollHeight).mockReturnValue(scrollHeight);
|
||||||
|
|
||||||
|
let capturedEntry: ScrollProviderModule.ScrollableEntry | undefined;
|
||||||
|
vi.spyOn(ScrollProviderModule, 'useScrollable').mockImplementation(
|
||||||
|
(entry, isActive) => {
|
||||||
|
if (isActive) {
|
||||||
|
capturedEntry = entry as ScrollProviderModule.ScrollableEntry;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { stdin } = renderWithProviders(
|
||||||
|
<Scrollable hasFocus={true} height={5}>
|
||||||
|
<Text>Content</Text>
|
||||||
|
</Scrollable>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure initial state using existing scrollBy method
|
||||||
|
act(() => {
|
||||||
|
// Reset to top first, then scroll to desired start position
|
||||||
|
capturedEntry!.scrollBy(-100);
|
||||||
|
if (initialScrollTop > 0) {
|
||||||
|
capturedEntry!.scrollBy(initialScrollTop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(capturedEntry!.getScrollState().scrollTop).toBe(
|
||||||
|
initialScrollTop,
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
stdin.write(keySequence);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(capturedEntry!.getScrollState().scrollTop).toBe(
|
||||||
|
expectedScrollTop,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
|||||||
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
||||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||||
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
||||||
|
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||||
|
|
||||||
interface ScrollableProps {
|
interface ScrollableProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -103,14 +104,38 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
|||||||
|
|
||||||
useKeypress(
|
useKeypress(
|
||||||
(key: Key) => {
|
(key: Key) => {
|
||||||
if (key.shift) {
|
const { scrollHeight, innerHeight } = sizeRef.current;
|
||||||
if (key.name === 'up') {
|
const scrollTop = getScrollTop();
|
||||||
scrollByWithAnimation(-1);
|
const maxScroll = Math.max(0, scrollHeight - innerHeight);
|
||||||
|
|
||||||
|
// Only capture scroll-up events if there's room;
|
||||||
|
// otherwise allow events to bubble.
|
||||||
|
if (scrollTop > 0) {
|
||||||
|
if (keyMatchers[Command.PAGE_UP](key)) {
|
||||||
|
scrollByWithAnimation(-innerHeight);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if (key.name === 'down') {
|
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||||
scrollByWithAnimation(1);
|
scrollByWithAnimation(-1);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only capture scroll-down events if there's room;
|
||||||
|
// otherwise allow events to bubble.
|
||||||
|
if (scrollTop < maxScroll) {
|
||||||
|
if (keyMatchers[Command.PAGE_DOWN](key)) {
|
||||||
|
scrollByWithAnimation(innerHeight);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||||
|
scrollByWithAnimation(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bubble keypress
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
{ isActive: hasFocus },
|
{ isActive: hasFocus },
|
||||||
);
|
);
|
||||||
@@ -137,7 +162,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
|||||||
[getScrollState, scrollByWithAnimation, hasFocusCallback, flashScrollbar],
|
[getScrollState, scrollByWithAnimation, hasFocusCallback, flashScrollbar],
|
||||||
);
|
);
|
||||||
|
|
||||||
useScrollable(scrollableEntry, hasFocus && ref.current !== null);
|
useScrollable(scrollableEntry, true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -186,9 +186,11 @@ function ScrollableList<T>(
|
|||||||
if (keyMatchers[Command.SCROLL_UP](key)) {
|
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||||
stopSmoothScroll();
|
stopSmoothScroll();
|
||||||
scrollByWithAnimation(-1);
|
scrollByWithAnimation(-1);
|
||||||
|
return true;
|
||||||
} else if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
} else if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||||
stopSmoothScroll();
|
stopSmoothScroll();
|
||||||
scrollByWithAnimation(1);
|
scrollByWithAnimation(1);
|
||||||
|
return true;
|
||||||
} else if (
|
} else if (
|
||||||
keyMatchers[Command.PAGE_UP](key) ||
|
keyMatchers[Command.PAGE_UP](key) ||
|
||||||
keyMatchers[Command.PAGE_DOWN](key)
|
keyMatchers[Command.PAGE_DOWN](key)
|
||||||
@@ -200,11 +202,15 @@ function ScrollableList<T>(
|
|||||||
: scrollState.scrollTop;
|
: scrollState.scrollTop;
|
||||||
const innerHeight = scrollState.innerHeight;
|
const innerHeight = scrollState.innerHeight;
|
||||||
smoothScrollTo(current + direction * innerHeight);
|
smoothScrollTo(current + direction * innerHeight);
|
||||||
|
return true;
|
||||||
} else if (keyMatchers[Command.SCROLL_HOME](key)) {
|
} else if (keyMatchers[Command.SCROLL_HOME](key)) {
|
||||||
smoothScrollTo(0);
|
smoothScrollTo(0);
|
||||||
|
return true;
|
||||||
} else if (keyMatchers[Command.SCROLL_END](key)) {
|
} else if (keyMatchers[Command.SCROLL_END](key)) {
|
||||||
smoothScrollTo(SCROLL_TO_ITEM_END);
|
smoothScrollTo(SCROLL_TO_ITEM_END);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
{ isActive: hasFocus },
|
{ isActive: hasFocus },
|
||||||
);
|
);
|
||||||
@@ -229,7 +235,7 @@ function ScrollableList<T>(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useScrollable(scrollableEntry, hasFocus);
|
useScrollable(scrollableEntry, true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -39,3 +39,9 @@ export const DEFAULT_BACKGROUND_OPACITY = 0.08;
|
|||||||
export const KEYBOARD_SHORTCUTS_URL =
|
export const KEYBOARD_SHORTCUTS_URL =
|
||||||
'https://geminicli.com/docs/cli/keyboard-shortcuts/';
|
'https://geminicli.com/docs/cli/keyboard-shortcuts/';
|
||||||
export const LRU_BUFFER_PERF_CACHE_LIMIT = 20000;
|
export const LRU_BUFFER_PERF_CACHE_LIMIT = 20000;
|
||||||
|
|
||||||
|
// Max lines to show for active shell output when not focused
|
||||||
|
export const ACTIVE_SHELL_MAX_LINES = 15;
|
||||||
|
|
||||||
|
// Max lines to preserve in history for completed shell commands
|
||||||
|
export const COMPLETED_SHELL_MAX_LINES = 15;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const findScrollableCandidates = (
|
|||||||
const candidates: Array<ScrollableEntry & { area: number }> = [];
|
const candidates: Array<ScrollableEntry & { area: number }> = [];
|
||||||
|
|
||||||
for (const entry of scrollables.values()) {
|
for (const entry of scrollables.values()) {
|
||||||
if (!entry.ref.current || !entry.hasFocus()) {
|
if (!entry.ref.current) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { act } from 'react';
|
import { act } from 'react';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { renderHook } from '../../test-utils/render.js';
|
import { renderHook } from '../../test-utils/render.js';
|
||||||
|
import { waitFor } from '../../test-utils/async.js';
|
||||||
import { ToolActionsProvider, useToolActions } from './ToolActionsContext.js';
|
import { ToolActionsProvider, useToolActions } from './ToolActionsContext.js';
|
||||||
import {
|
import {
|
||||||
type Config,
|
type Config,
|
||||||
@@ -155,7 +156,7 @@ describe('ToolActionsContext', () => {
|
|||||||
|
|
||||||
// Wait for IdeClient initialization in useEffect
|
// Wait for IdeClient initialization in useEffect
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await vi.waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
await waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
||||||
// Give React a chance to update state
|
// Give React a chance to update state
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
});
|
});
|
||||||
@@ -195,7 +196,7 @@ describe('ToolActionsContext', () => {
|
|||||||
|
|
||||||
// Wait for initialization
|
// Wait for initialization
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await vi.waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
await waitFor(() => expect(IdeClient.getInstance).toHaveBeenCalled());
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ vi.mock('node:os', async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock('node:crypto');
|
vi.mock('node:crypto');
|
||||||
vi.mock('../utils/textUtils.js');
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useShellCommandProcessor,
|
useShellCommandProcessor,
|
||||||
|
|||||||
@@ -245,5 +245,34 @@ describe('toolMapping', () => {
|
|||||||
expect(displayTool.status).toBe(ToolCallStatus.Canceled);
|
expect(displayTool.status).toBe(ToolCallStatus.Canceled);
|
||||||
expect(displayTool.resultDisplay).toBe('User cancelled');
|
expect(displayTool.resultDisplay).toBe('User cancelled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('propagates borderTop and borderBottom options correctly', () => {
|
||||||
|
const toolCall: ScheduledToolCall = {
|
||||||
|
status: 'scheduled',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall, {
|
||||||
|
borderTop: true,
|
||||||
|
borderBottom: false,
|
||||||
|
});
|
||||||
|
expect(result.borderTop).toBe(true);
|
||||||
|
expect(result.borderBottom).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets resultDisplay to undefined for pre-execution statuses', () => {
|
||||||
|
const toolCall: ScheduledToolCall = {
|
||||||
|
status: 'scheduled',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall);
|
||||||
|
expect(result.tools[0].resultDisplay).toBeUndefined();
|
||||||
|
expect(result.tools[0].status).toBe(ToolCallStatus.Pending);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -166,21 +166,27 @@ describe('keyMatchers', () => {
|
|||||||
{
|
{
|
||||||
command: Command.SCROLL_UP,
|
command: Command.SCROLL_UP,
|
||||||
positive: [createKey('up', { shift: true })],
|
positive: [createKey('up', { shift: true })],
|
||||||
negative: [createKey('up'), createKey('up', { ctrl: true })],
|
negative: [createKey('up')],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: Command.SCROLL_DOWN,
|
command: Command.SCROLL_DOWN,
|
||||||
positive: [createKey('down', { shift: true })],
|
positive: [createKey('down', { shift: true })],
|
||||||
negative: [createKey('down'), createKey('down', { ctrl: true })],
|
negative: [createKey('down')],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: Command.SCROLL_HOME,
|
command: Command.SCROLL_HOME,
|
||||||
positive: [createKey('home', { ctrl: true })],
|
positive: [
|
||||||
|
createKey('home', { ctrl: true }),
|
||||||
|
createKey('home', { shift: true }),
|
||||||
|
],
|
||||||
negative: [createKey('end'), createKey('home')],
|
negative: [createKey('end'), createKey('home')],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: Command.SCROLL_END,
|
command: Command.SCROLL_END,
|
||||||
positive: [createKey('end', { ctrl: true })],
|
positive: [
|
||||||
|
createKey('end', { ctrl: true }),
|
||||||
|
createKey('end', { shift: true }),
|
||||||
|
],
|
||||||
negative: [createKey('home'), createKey('end')],
|
negative: [createKey('home'), createKey('end')],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user