mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61227ea90a | |||
| 2e8d7831c6 | |||
| 3f4d5c07b3 | |||
| 4b19a83388 | |||
| 8c9b49ce8a | |||
| 9b6d47fd52 | |||
| 236af8bb0a | |||
| 013f984842 | |||
| f01890b073 | |||
| 7beccfa03c | |||
| a640766f10 | |||
| 9f55fb5010 | |||
| ea3d022c8b | |||
| 313688fd18 | |||
| 843b019cef | |||
| 80ef6f854b | |||
| 316349ca61 | |||
| 179010eb3b |
@@ -240,7 +240,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **`ui.useAlternateBuffer`** (boolean):
|
||||
- **Description:** Use an alternate screen buffer for the UI, preserving shell
|
||||
history.
|
||||
- **Default:** `true`
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`ui.incrementalRendering`** (boolean):
|
||||
|
||||
@@ -321,6 +321,9 @@ export class TestRig {
|
||||
selectedType: 'gemini-api-key',
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
useAlternateBuffer: true,
|
||||
},
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
sandbox:
|
||||
env['GEMINI_SANDBOX'] !== 'false' ? env['GEMINI_SANDBOX'] : false,
|
||||
|
||||
Generated
+39
-46
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -2047,9 +2047,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz",
|
||||
"integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.23.0.tgz",
|
||||
"integrity": "sha512-MCGd4K9aZKvuSqdoBkdMvZNcYXCkZRYVs/Gh92mdV5IHbctX9H9uIvd4X93+9g8tBbXv08sxc/QHXTzf8y65bA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1",
|
||||
@@ -2063,18 +2063,22 @@
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
"zod": "^3.25 || ^4.0",
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cfworker/json-schema": "^4.1.1"
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cfworker/json-schema": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2403,7 +2407,6 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2584,7 +2587,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2618,7 +2620,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2987,7 +2988,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -3021,7 +3021,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1"
|
||||
@@ -3074,7 +3073,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1",
|
||||
@@ -4285,7 +4283,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4573,7 +4570,6 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -5497,7 +5493,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5933,7 +5928,8 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/array-includes": {
|
||||
"version": "3.1.9",
|
||||
@@ -7197,6 +7193,7 @@
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
@@ -8212,7 +8209,6 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8802,6 +8798,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -8811,6 +8808,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -8820,6 +8818,7 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -9073,6 +9072,7 @@
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
@@ -9091,6 +9091,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -9099,13 +9100,15 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/finalhandler/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -10316,7 +10319,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.5.tgz",
|
||||
"integrity": "sha512-mIDkZqtJbedL9XDOoqoJt3S8aGQVqEJYnCnSeLlYzkpUWCsSWC0hW40yJ0DLH86lcl8k5R5lv/9C2i/3746nWw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -13424,7 +13426,8 @@
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "3.0.0",
|
||||
@@ -13959,7 +13962,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13970,7 +13972,6 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -16189,7 +16190,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16354,8 +16354,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -16363,7 +16362,6 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -16548,7 +16546,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16711,6 +16708,7 @@
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
@@ -16766,7 +16764,6 @@
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16883,7 +16880,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16897,7 +16893,6 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -17604,23 +17599,22 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.6",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
||||
"version": "3.25.0",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
|
||||
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.25 || ^4"
|
||||
}
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.2",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17910,12 +17904,12 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"ansi-regex": "^6.2.2",
|
||||
"clipboardy": "^5.0.0",
|
||||
@@ -18011,7 +18005,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"dependencies": {
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
@@ -18019,7 +18013,7 @@
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.3",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
|
||||
@@ -18146,7 +18140,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -18156,7 +18149,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
@@ -18167,10 +18160,10 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"zod": "^3.25.76"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.18.0-nightly.20251120.2231497b1"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.18.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -8,23 +8,26 @@
|
||||
|
||||
import './src/gemini.js';
|
||||
import { main } from './src/gemini.js';
|
||||
import { debugLogger, FatalError } from '@google/gemini-cli-core';
|
||||
import { FatalError, writeToStderr } from '@google/gemini-cli-core';
|
||||
import { runExitCleanup } from './src/utils/cleanup.js';
|
||||
|
||||
// --- Global Entry Point ---
|
||||
main().catch((error) => {
|
||||
main().catch(async (error) => {
|
||||
await runExitCleanup();
|
||||
|
||||
if (error instanceof FatalError) {
|
||||
let errorMessage = error.message;
|
||||
if (!process.env['NO_COLOR']) {
|
||||
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
||||
}
|
||||
debugLogger.error(errorMessage);
|
||||
writeToStderr(errorMessage + '\n');
|
||||
process.exit(error.exitCode);
|
||||
}
|
||||
debugLogger.error('An unexpected critical error occurred:');
|
||||
writeToStderr('An unexpected critical error occurred:');
|
||||
if (error instanceof Error) {
|
||||
debugLogger.error(error.stack);
|
||||
writeToStderr(error.stack + '\n');
|
||||
} else {
|
||||
debugLogger.error(String(error));
|
||||
writeToStderr(String(error) + '\n');
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"description": "Gemini CLI",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,13 +25,13 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.18.0-nightly.20251120.2231497b1"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.18.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"ansi-regex": "^6.2.2",
|
||||
"clipboardy": "^5.0.0",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { enableCommand } from './extensions/enable.js';
|
||||
import { linkCommand } from './extensions/link.js';
|
||||
import { newCommand } from './extensions/new.js';
|
||||
import { validateCommand } from './extensions/validate.js';
|
||||
import { initializeOutputListenersAndFlush } from '../gemini.js';
|
||||
|
||||
export const extensionsCommand: CommandModule = {
|
||||
command: 'extensions <command>',
|
||||
@@ -21,6 +22,7 @@ export const extensionsCommand: CommandModule = {
|
||||
describe: 'Manage Gemini CLI extensions.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.middleware(() => initializeOutputListenersAndFlush())
|
||||
.command(installCommand)
|
||||
.command(uninstallCommand)
|
||||
.command(listCommand)
|
||||
|
||||
@@ -56,6 +56,9 @@ vi.mock('../../config/extensions/consent.js', () => ({
|
||||
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
||||
promptForSetting: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions disable command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface DisableArgs {
|
||||
name: string;
|
||||
@@ -81,5 +82,6 @@ export const disableCommand: CommandModule = {
|
||||
name: argv['name'] as string,
|
||||
scope: argv['scope'] as string,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -58,6 +58,9 @@ vi.mock('../../config/extension-manager.js');
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../config/extensions/consent.js');
|
||||
vi.mock('../../config/extensions/extensionSettings.js');
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions enable command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface EnableArgs {
|
||||
name: string;
|
||||
@@ -86,5 +87,6 @@ export const enableCommand: CommandModule = {
|
||||
name: argv['name'] as string,
|
||||
scope: argv['scope'] as string,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@types/node": "^20.11.25"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ vi.mock('node:fs/promises', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions install command', () => {
|
||||
it('should fail if no source is provided', () => {
|
||||
const validationParser = yargs([]).command(installCommand).fail(false);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface InstallArgs {
|
||||
source: string;
|
||||
@@ -130,5 +131,6 @@ export const installCommand: CommandModule = {
|
||||
allowPreRelease: argv['pre-release'] as boolean | undefined,
|
||||
consent: argv['consent'] as boolean | undefined,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,6 +52,9 @@ vi.mock('../../config/extensions/consent.js', () => ({
|
||||
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
||||
promptForSetting: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions link command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { requestConsentNonInteractive } from '../../config/extensions/consent.js
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface InstallArgs {
|
||||
path: string;
|
||||
@@ -60,5 +61,6 @@ export const linkCommand: CommandModule = {
|
||||
await handleLink({
|
||||
path: argv['path'] as string,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,6 +44,9 @@ vi.mock('../../config/extensions/consent.js', () => ({
|
||||
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
||||
promptForSetting: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions list command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
export async function handleList() {
|
||||
try {
|
||||
@@ -45,5 +46,6 @@ export const listCommand: CommandModule = {
|
||||
builder: (yargs) => yargs,
|
||||
handler: async () => {
|
||||
await handleList();
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,6 +11,9 @@ import * as fsPromises from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedFs = vi.mocked(fsPromises);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { join, dirname, basename } from 'node:path';
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface NewArgs {
|
||||
path: string;
|
||||
@@ -100,5 +101,6 @@ export const newCommand: CommandModule = {
|
||||
path: args['path'] as string,
|
||||
template: args['template'] as string | undefined,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -75,6 +75,9 @@ vi.mock('../../config/extensions/consent.js', () => ({
|
||||
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
||||
promptForSetting: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions uninstall command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { requestConsentNonInteractive } from '../../config/extensions/consent.js
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface UninstallArgs {
|
||||
names: string[]; // can be extension names or source URLs.
|
||||
@@ -72,5 +73,6 @@ export const uninstallCommand: CommandModule = {
|
||||
await handleUninstall({
|
||||
names: argv['names'] as string[],
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,6 +56,9 @@ vi.mock('../../config/extensions/consent.js', () => ({
|
||||
vi.mock('../../config/extensions/extensionSettings.js', () => ({
|
||||
promptForSetting: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions update command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface UpdateArgs {
|
||||
name?: string;
|
||||
@@ -144,5 +145,6 @@ export const updateCommand: CommandModule = {
|
||||
name: argv['name'] as string | undefined,
|
||||
all: argv['all'] as boolean | undefined,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,6 +13,10 @@ import path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('extensions validate command', () => {
|
||||
it('should fail if no path is provided', () => {
|
||||
const validationParser = yargs([]).command(validateCommand).fail(false);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
interface ValidateArgs {
|
||||
path: string;
|
||||
@@ -101,5 +102,6 @@ export const validateCommand: CommandModule = {
|
||||
await handleValidate({
|
||||
path: args['path'] as string,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('mcp command', () => {
|
||||
command: vi.fn().mockReturnThis(),
|
||||
demandCommand: vi.fn().mockReturnThis(),
|
||||
version: vi.fn().mockReturnThis(),
|
||||
middleware: vi.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
(mcpCommand.builder as (y: Argv) => Argv)(mockYargs as unknown as Argv);
|
||||
|
||||
@@ -9,12 +9,14 @@ import type { CommandModule, Argv } from 'yargs';
|
||||
import { addCommand } from './mcp/add.js';
|
||||
import { removeCommand } from './mcp/remove.js';
|
||||
import { listCommand } from './mcp/list.js';
|
||||
import { initializeOutputListenersAndFlush } from '../gemini.js';
|
||||
|
||||
export const mcpCommand: CommandModule = {
|
||||
command: 'mcp',
|
||||
describe: 'Manage MCP servers',
|
||||
builder: (yargs: Argv) =>
|
||||
yargs
|
||||
.middleware(() => initializeOutputListenersAndFlush())
|
||||
.command(addCommand)
|
||||
.command(removeCommand)
|
||||
.command(listCommand)
|
||||
|
||||
@@ -10,6 +10,10 @@ import { addCommand } from './add.js';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { debugLogger, type MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
async function addMcpServer(
|
||||
name: string,
|
||||
@@ -230,5 +231,6 @@ export const addCommand: CommandModule = {
|
||||
excludeTools: argv['excludeTools'] as string[] | undefined,
|
||||
},
|
||||
);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,6 +44,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
});
|
||||
vi.mock('@modelcontextprotocol/sdk/client/index.js');
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedGetUserExtensionsDir =
|
||||
ExtensionStorage.getUserExtensionsDir as Mock;
|
||||
const mockedLoadSettings = loadSettings as Mock;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
const COLOR_GREEN = '\u001b[32m';
|
||||
const COLOR_YELLOW = '\u001b[33m';
|
||||
@@ -145,5 +146,6 @@ export const listCommand: CommandModule = {
|
||||
describe: 'List all configured MCP servers',
|
||||
handler: async () => {
|
||||
await listMcpServers();
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,6 +26,10 @@ vi.mock('fs/promises', () => ({
|
||||
writeFile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../utils.js', () => ({
|
||||
exitCli: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('mcp remove command', () => {
|
||||
describe('unit tests with mocks', () => {
|
||||
let parser: Argv;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
async function removeMcpServer(
|
||||
name: string,
|
||||
@@ -57,5 +58,6 @@ export const removeCommand: CommandModule = {
|
||||
await removeMcpServer(argv['name'] as string, {
|
||||
scope: argv['scope'] as string,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { runExitCleanup } from '../utils/cleanup.js';
|
||||
|
||||
export async function exitCli(exitCode = 0) {
|
||||
await runExitCleanup();
|
||||
process.exit(exitCode);
|
||||
}
|
||||
@@ -313,19 +313,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// If yargs handled --help/--version it will have exited; nothing to do here.
|
||||
|
||||
// Handle case where MCP subcommands are executed - they should exit the process
|
||||
// and not return to main CLI logic
|
||||
if (
|
||||
result._.length > 0 &&
|
||||
(result._[0] === 'mcp' || result._[0] === 'extensions')
|
||||
) {
|
||||
// MCP commands handle their own execution and process exit
|
||||
await runExitCleanup();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Normalize query args: handle both quoted "@path file" and unquoted @path file
|
||||
const queryArg = (result as { query?: string | string[] | undefined }).query;
|
||||
const q: string | undefined = Array.isArray(queryArg)
|
||||
|
||||
@@ -516,7 +516,7 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Use Alternate Screen Buffer',
|
||||
category: 'UI',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
default: false,
|
||||
description:
|
||||
'Use an alternate screen buffer for the UI, preserving shell history.',
|
||||
showInDialog: true,
|
||||
|
||||
@@ -40,6 +40,28 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
recordSlowRender: vi.fn(),
|
||||
writeToStdout: vi.fn((...args) =>
|
||||
process.stdout.write(
|
||||
...(args as Parameters<typeof process.stdout.write>),
|
||||
),
|
||||
),
|
||||
patchStdio: vi.fn(() => () => {}),
|
||||
createInkStdio: vi.fn(() => ({
|
||||
stdout: {
|
||||
write: vi.fn((...args) =>
|
||||
process.stdout.write(
|
||||
...(args as Parameters<typeof process.stdout.write>),
|
||||
),
|
||||
),
|
||||
columns: 80,
|
||||
rows: 24,
|
||||
on: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
},
|
||||
stderr: {
|
||||
write: vi.fn(),
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -149,35 +171,6 @@ vi.mock('./ui/utils/mouse.js', () => ({
|
||||
isIncompleteMouseSequence: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./utils/stdio.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./utils/stdio.js')>();
|
||||
return {
|
||||
...actual,
|
||||
writeToStdout: vi.fn((...args) =>
|
||||
process.stdout.write(
|
||||
...(args as Parameters<typeof process.stdout.write>),
|
||||
),
|
||||
),
|
||||
patchStdio: vi.fn(() => () => {}),
|
||||
createInkStdio: vi.fn(() => ({
|
||||
stdout: {
|
||||
write: vi.fn((...args) =>
|
||||
process.stdout.write(
|
||||
...(args as Parameters<typeof process.stdout.write>),
|
||||
),
|
||||
),
|
||||
columns: 80,
|
||||
rows: 24,
|
||||
on: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
},
|
||||
stderr: {
|
||||
write: vi.fn(),
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('gemini.tsx main function', () => {
|
||||
let originalEnvGeminiSandbox: string | undefined;
|
||||
let originalEnvSandbox: string | undefined;
|
||||
@@ -512,6 +505,7 @@ describe('startInteractiveUI', () => {
|
||||
merged: {
|
||||
ui: {
|
||||
hideWindowTitle: false,
|
||||
useAlternateBuffer: true,
|
||||
},
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
+26
-19
@@ -33,13 +33,11 @@ import {
|
||||
runExitCleanup,
|
||||
} from './utils/cleanup.js';
|
||||
import { getCliVersion } from './utils/version.js';
|
||||
import type {
|
||||
Config,
|
||||
ResumedSessionData,
|
||||
OutputPayload,
|
||||
ConsoleLogPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
type OutputPayload,
|
||||
type ConsoleLogPayload,
|
||||
sessionId,
|
||||
logUserPrompt,
|
||||
AuthType,
|
||||
@@ -49,6 +47,15 @@ import {
|
||||
recordSlowRender,
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
createInkStdio,
|
||||
patchStdio,
|
||||
writeToStdout,
|
||||
writeToStderr,
|
||||
disableMouseEvents,
|
||||
enableMouseEvents,
|
||||
enterAlternateScreen,
|
||||
disableLineWrapping,
|
||||
shouldEnterAlternateScreen,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
initializeApp,
|
||||
@@ -81,16 +88,8 @@ import { deleteSession, listSessions } from './utils/sessions.js';
|
||||
import { ExtensionManager } from './config/extension-manager.js';
|
||||
import { createPolicyUpdater } from './config/policy.js';
|
||||
import { requestConsentNonInteractive } from './config/extensions/consent.js';
|
||||
import { disableMouseEvents, enableMouseEvents } from './ui/utils/mouse.js';
|
||||
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
||||
import ansiEscapes from 'ansi-escapes';
|
||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
import {
|
||||
createInkStdio,
|
||||
patchStdio,
|
||||
writeToStderr,
|
||||
writeToStdout,
|
||||
} from './utils/stdio.js';
|
||||
|
||||
import { profiler } from './ui/components/DebugProfiler.js';
|
||||
|
||||
@@ -178,8 +177,10 @@ export async function startInteractiveUI(
|
||||
// as there is no benefit of alternate buffer mode when using a screen reader
|
||||
// and the Ink alternate buffer mode requires line wrapping harmful to
|
||||
// screen readers.
|
||||
const useAlternateBuffer =
|
||||
isAlternateBufferEnabled(settings) && !config.getScreenReader();
|
||||
const useAlternateBuffer = shouldEnterAlternateScreen(
|
||||
isAlternateBufferEnabled(settings),
|
||||
config.getScreenReader(),
|
||||
);
|
||||
const mouseEventsEnabled = useAlternateBuffer;
|
||||
if (mouseEventsEnabled) {
|
||||
enableMouseEvents();
|
||||
@@ -483,8 +484,14 @@ export async function main() {
|
||||
// input showing up in the output.
|
||||
process.stdin.setRawMode(true);
|
||||
|
||||
if (isAlternateBufferEnabled(settings)) {
|
||||
writeToStdout(ansiEscapes.enterAlternativeScreen);
|
||||
if (
|
||||
shouldEnterAlternateScreen(
|
||||
isAlternateBufferEnabled(settings),
|
||||
config.getScreenReader(),
|
||||
)
|
||||
) {
|
||||
enterAlternateScreen();
|
||||
disableLineWrapping();
|
||||
|
||||
// Ink will cleanup so there is no need for us to manually cleanup.
|
||||
}
|
||||
@@ -627,7 +634,7 @@ function setWindowTitle(title: string, settings: LoadedSettings) {
|
||||
}
|
||||
}
|
||||
|
||||
function initializeOutputListenersAndFlush() {
|
||||
export function initializeOutputListenersAndFlush() {
|
||||
// If there are no listeners for output, make sure we flush so output is not
|
||||
// lost.
|
||||
if (coreEvents.listenerCount(CoreEvent.Output) === 0) {
|
||||
|
||||
@@ -176,7 +176,7 @@ export const renderWithProviders = (
|
||||
width,
|
||||
mouseEventsEnabled = false,
|
||||
config = configProxy as unknown as Config,
|
||||
useAlternateBuffer,
|
||||
useAlternateBuffer = true,
|
||||
uiActions,
|
||||
}: {
|
||||
shellFocus?: boolean;
|
||||
|
||||
@@ -54,6 +54,23 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
...actual,
|
||||
coreEvents: mockCoreEvents,
|
||||
IdeClient: mockIdeClient,
|
||||
writeToStdout: vi.fn((...args) =>
|
||||
process.stdout.write(
|
||||
...(args as Parameters<typeof process.stdout.write>),
|
||||
),
|
||||
),
|
||||
writeToStderr: vi.fn((...args) =>
|
||||
process.stderr.write(
|
||||
...(args as Parameters<typeof process.stderr.write>),
|
||||
),
|
||||
),
|
||||
patchStdio: vi.fn(() => () => {}),
|
||||
createInkStdio: vi.fn(() => ({
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
})),
|
||||
enableMouseEvents: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
};
|
||||
});
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
@@ -122,23 +139,6 @@ vi.mock('../utils/events.js');
|
||||
vi.mock('../utils/handleAutoUpdate.js');
|
||||
vi.mock('./utils/ConsolePatcher.js');
|
||||
vi.mock('../utils/cleanup.js');
|
||||
vi.mock('./utils/mouse.js', () => ({
|
||||
enableMouseEvents: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils/stdio.js', () => ({
|
||||
writeToStdout: vi.fn((...args) =>
|
||||
process.stdout.write(...(args as Parameters<typeof process.stdout.write>)),
|
||||
),
|
||||
writeToStderr: vi.fn((...args) =>
|
||||
process.stderr.write(...(args as Parameters<typeof process.stderr.write>)),
|
||||
),
|
||||
patchStdio: vi.fn(() => () => {}),
|
||||
createInkStdio: vi.fn(() => ({
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
})),
|
||||
}));
|
||||
|
||||
import { useHistory } from './hooks/useHistoryManager.js';
|
||||
import { useThemeCommand } from './hooks/useThemeCommand.js';
|
||||
@@ -163,10 +163,13 @@ import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
||||
import { useKeypress, type Key } from './hooks/useKeypress.js';
|
||||
import { measureElement } from 'ink';
|
||||
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||
import {
|
||||
ShellExecutionService,
|
||||
writeToStdout,
|
||||
enableMouseEvents,
|
||||
disableMouseEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||
import { enableMouseEvents, disableMouseEvents } from './utils/mouse.js';
|
||||
import { writeToStdout } from '../utils/stdio.js';
|
||||
|
||||
describe('AppContainer State Management', () => {
|
||||
let mockConfig: Config;
|
||||
|
||||
@@ -52,6 +52,12 @@ import {
|
||||
refreshServerHierarchicalMemory,
|
||||
type ModelChangedPayload,
|
||||
type MemoryChangedPayload,
|
||||
writeToStdout,
|
||||
disableMouseEvents,
|
||||
enterAlternateScreen,
|
||||
enableMouseEvents,
|
||||
disableLineWrapping,
|
||||
shouldEnterAlternateScreen,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import process from 'node:process';
|
||||
@@ -92,6 +98,7 @@ import { appEvents, AppEvent } from '../utils/events.js';
|
||||
import { type UpdateObject } from './utils/updateCheck.js';
|
||||
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
||||
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
||||
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||
import { useSessionStats } from './contexts/SessionContext.js';
|
||||
@@ -106,11 +113,9 @@ import { type ExtensionManager } from '../config/extension-manager.js';
|
||||
import { requestConsentInteractive } from '../config/extensions/consent.js';
|
||||
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||
import { disableMouseEvents, enableMouseEvents } from './utils/mouse.js';
|
||||
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
||||
import { useSettings } from './contexts/SettingsContext.js';
|
||||
import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js';
|
||||
import { writeToStdout } from '../utils/stdio.js';
|
||||
|
||||
const WARNING_PROMPT_DURATION_MS = 1000;
|
||||
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
|
||||
@@ -372,16 +377,19 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
setHistoryRemountKey((prev) => prev + 1);
|
||||
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
|
||||
const handleEditorClose = useCallback(() => {
|
||||
if (isAlternateBuffer) {
|
||||
if (
|
||||
shouldEnterAlternateScreen(isAlternateBuffer, config.getScreenReader())
|
||||
) {
|
||||
// The editor may have exited alternate buffer mode so we need to
|
||||
// enter it again to be safe.
|
||||
writeToStdout(ansiEscapes.enterAlternativeScreen);
|
||||
enterAlternateScreen();
|
||||
enableMouseEvents();
|
||||
disableLineWrapping();
|
||||
app.rerender();
|
||||
}
|
||||
enableSupportedProtocol();
|
||||
refreshStatic();
|
||||
}, [refreshStatic, isAlternateBuffer, app]);
|
||||
}, [refreshStatic, isAlternateBuffer, app, config]);
|
||||
|
||||
useEffect(() => {
|
||||
coreEvents.on(CoreEvent.ExternalEditorClosed, handleEditorClose);
|
||||
@@ -458,12 +466,12 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
config.isBrowserLaunchSuppressed()
|
||||
) {
|
||||
await runExitCleanup();
|
||||
debugLogger.log(`
|
||||
writeToStdout(`
|
||||
----------------------------------------------------------------
|
||||
Logging in with Google... Please restart Gemini CLI to continue.
|
||||
Logging in with Google... Restarting Gemini CLI to continue.
|
||||
----------------------------------------------------------------
|
||||
`);
|
||||
process.exit(0);
|
||||
process.exit(RELAUNCH_EXIT_CODE);
|
||||
}
|
||||
}
|
||||
setAuthState(AuthState.Authenticated);
|
||||
@@ -705,6 +713,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
} = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
@@ -1104,6 +1113,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(
|
||||
streamingState,
|
||||
settings.merged.ui?.customWittyPhrases,
|
||||
!!activePtyId && !embeddedShellFocused,
|
||||
lastOutputTime,
|
||||
);
|
||||
|
||||
const handleGlobalKeypress = useCallback(
|
||||
|
||||
@@ -25,6 +25,7 @@ import { validateAuthMethodWithSettings } from './useAuth.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import { clearCachedCredentialFile } from '@google/gemini-cli-core';
|
||||
import { Text } from 'ink';
|
||||
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||
|
||||
// Mocks
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
@@ -229,6 +230,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('exits process for Login with Google when browser is suppressed', async () => {
|
||||
vi.useFakeTimers();
|
||||
const exitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
@@ -241,14 +243,14 @@ describe('AuthDialog', () => {
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Please restart Gemini CLI'),
|
||||
);
|
||||
expect(exitSpy).toHaveBeenCalledWith(0);
|
||||
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||
@@ -17,13 +17,13 @@ import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
AuthType,
|
||||
clearCachedCredentialFile,
|
||||
debugLogger,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { AuthState } from '../types.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import { validateAuthMethodWithSettings } from './useAuth.js';
|
||||
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||
|
||||
interface AuthDialogProps {
|
||||
config: Config;
|
||||
@@ -40,6 +40,7 @@ export function AuthDialog({
|
||||
authError,
|
||||
onAuthError,
|
||||
}: AuthDialogProps): React.JSX.Element {
|
||||
const [exiting, setExiting] = useState(false);
|
||||
let items = [
|
||||
{
|
||||
label: 'Login with Google',
|
||||
@@ -111,6 +112,9 @@ export function AuthDialog({
|
||||
|
||||
const onSelect = useCallback(
|
||||
async (authType: AuthType | undefined, scope: LoadableSettingScope) => {
|
||||
if (exiting) {
|
||||
return;
|
||||
}
|
||||
if (authType) {
|
||||
await clearCachedCredentialFile();
|
||||
|
||||
@@ -119,15 +123,12 @@ export function AuthDialog({
|
||||
authType === AuthType.LOGIN_WITH_GOOGLE &&
|
||||
config.isBrowserLaunchSuppressed()
|
||||
) {
|
||||
runExitCleanup();
|
||||
debugLogger.log(
|
||||
`
|
||||
----------------------------------------------------------------
|
||||
Logging in with Google... Please restart Gemini CLI to continue.
|
||||
----------------------------------------------------------------
|
||||
`,
|
||||
);
|
||||
process.exit(0);
|
||||
setExiting(true);
|
||||
setTimeout(async () => {
|
||||
await runExitCleanup();
|
||||
process.exit(RELAUNCH_EXIT_CODE);
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (authType === AuthType.USE_GEMINI) {
|
||||
@@ -136,7 +137,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
}
|
||||
setAuthState(AuthState.Unauthenticated);
|
||||
},
|
||||
[settings, config, setAuthState],
|
||||
[settings, config, setAuthState, exiting],
|
||||
);
|
||||
|
||||
const handleAuthSelect = (authMethod: AuthType) => {
|
||||
@@ -169,6 +170,23 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
if (exiting) {
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.focused}
|
||||
flexDirection="row"
|
||||
padding={1}
|
||||
width="100%"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Text color={theme.text.primary}>
|
||||
Logging in with Google... Restarting Gemini CLI to continue.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const persistentStateMock = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
@@ -25,7 +26,7 @@ vi.mock('../utils/terminalSetup.js', () => ({
|
||||
describe('<AppHeader />', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
persistentStateMock.get.mockReturnValue(0);
|
||||
persistentStateMock.get.mockReturnValue({});
|
||||
});
|
||||
|
||||
it('should render the banner with default text', () => {
|
||||
@@ -146,8 +147,8 @@ describe('<AppHeader />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should increment the shown count when default banner is displayed', () => {
|
||||
persistentStateMock.get.mockReturnValue(0);
|
||||
it('should increment the version count when default banner is displayed', () => {
|
||||
persistentStateMock.get.mockReturnValue({});
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
@@ -163,7 +164,12 @@ describe('<AppHeader />', () => {
|
||||
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||
'defaultBannerShownCount',
|
||||
1,
|
||||
{
|
||||
[crypto
|
||||
.createHash('sha256')
|
||||
.update(uiState.bannerData.defaultText)
|
||||
.digest('hex')]: 1,
|
||||
},
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -10,9 +10,8 @@ import { Tips } from './Tips.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { persistentState } from '../../utils/persistentState.js';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Banner } from './Banner.js';
|
||||
import { useBanner } from '../hooks/useBanner.js';
|
||||
|
||||
interface AppHeaderProps {
|
||||
version: string;
|
||||
@@ -23,27 +22,7 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
const config = useConfig();
|
||||
const { nightly, mainAreaWidth, bannerData, bannerVisible } = useUIState();
|
||||
|
||||
const [defaultBannerShownCount] = useState(
|
||||
() => persistentState.get('defaultBannerShownCount') || 0,
|
||||
);
|
||||
|
||||
const { defaultText, warningText } = bannerData;
|
||||
|
||||
const showDefaultBanner =
|
||||
warningText === '' &&
|
||||
!config.getPreviewFeatures() &&
|
||||
defaultBannerShownCount < 5;
|
||||
|
||||
const bannerText = showDefaultBanner ? defaultText : warningText;
|
||||
|
||||
const hasIncrementedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (showDefaultBanner && defaultText && !hasIncrementedRef.current) {
|
||||
hasIncrementedRef.current = true;
|
||||
const current = persistentState.get('defaultBannerShownCount') || 0;
|
||||
persistentState.set('defaultBannerShownCount', current + 1);
|
||||
}
|
||||
}, [showDefaultBanner, defaultText]);
|
||||
const { bannerText } = useBanner(bannerData, config);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
@@ -54,7 +33,7 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
<Banner
|
||||
width={mainAreaWidth}
|
||||
bannerText={bannerText}
|
||||
isWarning={warningText !== ''}
|
||||
isWarning={bannerData.warningText !== ''}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -18,6 +18,8 @@ import { ApiAuthDialog } from '../auth/ApiAuthDialog.js';
|
||||
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
|
||||
import { PrivacyNotice } from '../privacy/PrivacyNotice.js';
|
||||
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||
import { ModelDialog } from './ModelDialog.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
@@ -137,7 +139,10 @@ export const DialogManager = ({
|
||||
<SettingsDialog
|
||||
settings={settings}
|
||||
onSelect={() => uiActions.closeSettingsDialog()}
|
||||
onRestartRequest={() => process.exit(0)}
|
||||
onRestartRequest={async () => {
|
||||
await runExitCleanup();
|
||||
process.exit(RELAUNCH_EXIT_CODE);
|
||||
}}
|
||||
availableTerminalHeight={terminalHeight - staticExtraHeight}
|
||||
config={config}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
||||
|
||||
interface LoadingIndicatorProps {
|
||||
currentLoadingPhrase?: string;
|
||||
@@ -36,7 +37,12 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const primaryText = thought?.subject || currentLoadingPhrase;
|
||||
// Prioritize the interactive shell waiting phrase over the thought subject
|
||||
// because it conveys an actionable state for the user (waiting for input).
|
||||
const primaryText =
|
||||
currentLoadingPhrase === INTERACTIVE_SHELL_WAITING_PHRASE
|
||||
? currentLoadingPhrase
|
||||
: thought?.subject || currentLoadingPhrase;
|
||||
|
||||
const cancelAndTimerContent =
|
||||
streamingState !== StreamingState.WaitingForConfirmation
|
||||
|
||||
@@ -12,7 +12,7 @@ import { theme } from '../semantic-colors.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
@@ -127,7 +127,7 @@ export function ProQuotaDialog({
|
||||
<RadioButtonSelect items={items} onSelect={handleSelect} />
|
||||
</Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{failedModel === PREVIEW_GEMINI_MODEL && !isModelNotFoundError
|
||||
{fallbackModel === DEFAULT_GEMINI_MODEL && !isModelNotFoundError
|
||||
? 'Note: We will periodically retry Preview Model to see if congestion has cleared.'
|
||||
: 'Note: You can always use /model to select a different option.'}
|
||||
</Text>
|
||||
|
||||
@@ -9,7 +9,11 @@ import { Box, Text, type DOMElement } from 'ink';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||
import { StickyHeader } from '../StickyHeader.js';
|
||||
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
|
||||
import {
|
||||
SHELL_COMMAND_NAME,
|
||||
SHELL_NAME,
|
||||
SHELL_FOCUS_HINT_DELAY_MS,
|
||||
} from '../../constants.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
||||
import { useUIActions } from '../../contexts/UIActionsContext.js';
|
||||
@@ -104,7 +108,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setShowFocusHint(true);
|
||||
}, 5000);
|
||||
}, SHELL_FOCUS_HINT_DELAY_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [lastUpdateTime]);
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box } from 'ink';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { StickyHeader } from '../StickyHeader.js';
|
||||
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
||||
@@ -14,7 +15,17 @@ import {
|
||||
ToolInfo,
|
||||
TrailingIndicator,
|
||||
type TextEmphasis,
|
||||
STATUS_INDICATOR_WIDTH,
|
||||
} from './ToolShared.js';
|
||||
import {
|
||||
SHELL_COMMAND_NAME,
|
||||
SHELL_FOCUS_HINT_DELAY_MS,
|
||||
} from '../../constants.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { useInactivityTimer } from '../../hooks/useInactivityTimer.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||
|
||||
export type { TextEmphasis };
|
||||
|
||||
@@ -26,6 +37,10 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
|
||||
isFirst: boolean;
|
||||
borderColor: string;
|
||||
borderDimColor: boolean;
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
ptyId?: number;
|
||||
config?: Config;
|
||||
}
|
||||
|
||||
export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
@@ -40,41 +55,96 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
isFirst,
|
||||
borderColor,
|
||||
borderDimColor,
|
||||
}) => (
|
||||
<Box flexDirection="column" width={terminalWidth}>
|
||||
<StickyHeader
|
||||
width={terminalWidth}
|
||||
isFirst={isFirst}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
>
|
||||
<ToolStatusIndicator status={status} name={name} />
|
||||
<ToolInfo
|
||||
name={name}
|
||||
status={status}
|
||||
description={description}
|
||||
emphasis={emphasis}
|
||||
/>
|
||||
{emphasis === 'high' && <TrailingIndicator />}
|
||||
</StickyHeader>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderTop={false}
|
||||
borderBottom={false}
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
paddingX={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
<ToolResultDisplay
|
||||
resultDisplay={resultDisplay}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
||||
/>
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
ptyId,
|
||||
config,
|
||||
}) => {
|
||||
const isThisShellFocused =
|
||||
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
|
||||
status === ToolCallStatus.Executing &&
|
||||
ptyId === activeShellPtyId &&
|
||||
embeddedShellFocused;
|
||||
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
|
||||
const [userHasFocused, setUserHasFocused] = useState(false);
|
||||
const showFocusHint = useInactivityTimer(
|
||||
!!lastUpdateTime,
|
||||
lastUpdateTime ? lastUpdateTime.getTime() : 0,
|
||||
SHELL_FOCUS_HINT_DELAY_MS,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (resultDisplay) {
|
||||
setLastUpdateTime(new Date());
|
||||
}
|
||||
}, [resultDisplay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isThisShellFocused) {
|
||||
setUserHasFocused(true);
|
||||
}
|
||||
}, [isThisShellFocused]);
|
||||
|
||||
const isThisShellFocusable =
|
||||
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
|
||||
status === ToolCallStatus.Executing &&
|
||||
config?.getEnableInteractiveShell();
|
||||
|
||||
const shouldShowFocusHint =
|
||||
isThisShellFocusable && (showFocusHint || userHasFocused);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={terminalWidth}>
|
||||
<StickyHeader
|
||||
width={terminalWidth}
|
||||
isFirst={isFirst}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
>
|
||||
<ToolStatusIndicator status={status} name={name} />
|
||||
<ToolInfo
|
||||
name={name}
|
||||
status={status}
|
||||
description={description}
|
||||
emphasis={emphasis}
|
||||
/>
|
||||
{shouldShowFocusHint && (
|
||||
<Box marginLeft={1} flexShrink={0}>
|
||||
<Text color={theme.text.accent}>
|
||||
{isThisShellFocused ? '(Focused)' : '(ctrl+f to focus)'}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{emphasis === 'high' && <TrailingIndicator />}
|
||||
</StickyHeader>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderTop={false}
|
||||
borderBottom={false}
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
paddingX={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
<ToolResultDisplay
|
||||
resultDisplay={resultDisplay}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
renderOutputAsMarkdown={renderOutputAsMarkdown}
|
||||
/>
|
||||
{isThisShellFocused && config && (
|
||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
||||
<ShellInputPrompt
|
||||
activeShellPtyId={activeShellPtyId ?? null}
|
||||
focus={embeddedShellFocused}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,8 @@ export const SHELL_NAME = 'Shell';
|
||||
// usage.
|
||||
export const MAX_GEMINI_MESSAGE_LINES = 65536;
|
||||
|
||||
export const SHELL_FOCUS_HINT_DELAY_MS = 5000;
|
||||
|
||||
// Tool status symbols used in ToolMessage component
|
||||
export const TOOL_STATUS = {
|
||||
SUCCESS: '✓',
|
||||
|
||||
@@ -76,6 +76,8 @@ export const useShellCommandProcessor = (
|
||||
terminalHeight?: number,
|
||||
) => {
|
||||
const [activeShellPtyId, setActiveShellPtyId] = useState<number | null>(null);
|
||||
const [lastShellOutputTime, setLastShellOutputTime] = useState<number>(0);
|
||||
|
||||
const handleShellCommand = useCallback(
|
||||
(rawQuery: PartListUnion, abortSignal: AbortSignal): boolean => {
|
||||
if (typeof rawQuery !== 'string' || rawQuery.trim() === '') {
|
||||
@@ -202,6 +204,7 @@ export const useShellCommandProcessor = (
|
||||
|
||||
// Throttle pending UI updates, but allow forced updates.
|
||||
if (shouldUpdate) {
|
||||
setLastShellOutputTime(Date.now());
|
||||
setPendingHistoryItem((prevItem) => {
|
||||
if (prevItem?.type === 'tool_group') {
|
||||
return {
|
||||
@@ -366,5 +369,5 @@ export const useShellCommandProcessor = (
|
||||
],
|
||||
);
|
||||
|
||||
return { handleShellCommand, activeShellPtyId };
|
||||
return { handleShellCommand, activeShellPtyId, lastShellOutputTime };
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
export const isAlternateBufferEnabled = (settings: LoadedSettings): boolean =>
|
||||
settings.merged.ui?.useAlternateBuffer !== false;
|
||||
settings.merged.ui?.useAlternateBuffer === true;
|
||||
|
||||
export const useAlternateBuffer = (): boolean => {
|
||||
const settings = useSettings();
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type MockedFunction,
|
||||
} from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useBanner } from './useBanner.js';
|
||||
import { persistentState } from '../../utils/persistentState.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
vi.mock('../../utils/persistentState.js', () => ({
|
||||
persistentState: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../semantic-colors.js', () => ({
|
||||
theme: {
|
||||
status: {
|
||||
warning: 'mock-warning-color',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../colors.js', () => ({
|
||||
Colors: {
|
||||
AccentBlue: 'mock-accent-blue',
|
||||
},
|
||||
}));
|
||||
|
||||
// Define the shape of the config methods used by this hook
|
||||
interface MockConfigShape {
|
||||
getPreviewFeatures: MockedFunction<() => boolean>;
|
||||
}
|
||||
|
||||
describe('useBanner', () => {
|
||||
let mockConfig: MockConfigShape;
|
||||
const mockedPersistentStateGet = persistentState.get as MockedFunction<
|
||||
typeof persistentState.get
|
||||
>;
|
||||
const mockedPersistentStateSet = persistentState.set as MockedFunction<
|
||||
typeof persistentState.set
|
||||
>;
|
||||
|
||||
const defaultBannerData = {
|
||||
defaultText: 'Standard Banner',
|
||||
warningText: '',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Initialize the mock config with default behavior
|
||||
mockConfig = {
|
||||
getPreviewFeatures: vi.fn().mockReturnValue(false),
|
||||
};
|
||||
|
||||
// Default persistentState behavior: return empty object (no counts)
|
||||
mockedPersistentStateGet.mockReturnValue({});
|
||||
});
|
||||
|
||||
it('should return warning text and warning color if warningText is present', () => {
|
||||
const data = { defaultText: 'Standard', warningText: 'Critical Error' };
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useBanner(data, mockConfig as unknown as Config),
|
||||
);
|
||||
|
||||
expect(result.current.bannerText).toBe('Critical Error');
|
||||
});
|
||||
|
||||
it('should NOT show default banner if preview features are enabled in config', () => {
|
||||
// Simulate Preview Features Enabled
|
||||
mockConfig.getPreviewFeatures.mockReturnValue(true);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useBanner(defaultBannerData, mockConfig as unknown as Config),
|
||||
);
|
||||
|
||||
// Should fall back to warningText (which is empty)
|
||||
expect(result.current.bannerText).toBe('');
|
||||
});
|
||||
|
||||
it('should hide banner if show count exceeds max limit (Legacy format)', () => {
|
||||
mockedPersistentStateGet.mockReturnValue({
|
||||
[crypto
|
||||
.createHash('sha256')
|
||||
.update(defaultBannerData.defaultText)
|
||||
.digest('hex')]: 5,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useBanner(defaultBannerData, mockConfig as unknown as Config),
|
||||
);
|
||||
|
||||
expect(result.current.bannerText).toBe('');
|
||||
});
|
||||
|
||||
it('should increment the persistent count when banner is shown', () => {
|
||||
const data = { defaultText: 'Tracker', warningText: '' };
|
||||
|
||||
// Current count is 1
|
||||
mockedPersistentStateGet.mockReturnValue({
|
||||
[crypto.createHash('sha256').update(data.defaultText).digest('hex')]: 1,
|
||||
});
|
||||
|
||||
renderHook(() => useBanner(data, mockConfig as unknown as Config));
|
||||
|
||||
// Expect set to be called with incremented count
|
||||
expect(mockedPersistentStateSet).toHaveBeenCalledWith(
|
||||
'defaultBannerShownCount',
|
||||
{
|
||||
[crypto.createHash('sha256').update(data.defaultText).digest('hex')]: 2,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT increment count if warning text is shown instead', () => {
|
||||
const data = { defaultText: 'Standard', warningText: 'Warning' };
|
||||
|
||||
renderHook(() => useBanner(data, mockConfig as unknown as Config));
|
||||
|
||||
// Since warning text takes precedence, default banner logic (and increment) is skipped
|
||||
expect(mockedPersistentStateSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle newline replacements', () => {
|
||||
const data = { defaultText: 'Line1\\nLine2', warningText: '' };
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useBanner(data, mockConfig as unknown as Config),
|
||||
);
|
||||
|
||||
expect(result.current.bannerText).toBe('Line1\nLine2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { persistentState } from '../../utils/persistentState.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const DEFAULT_MAX_BANNER_SHOWN_COUNT = 5;
|
||||
|
||||
interface BannerData {
|
||||
defaultText: string;
|
||||
warningText: string;
|
||||
}
|
||||
|
||||
export function useBanner(bannerData: BannerData, config: Config) {
|
||||
const { defaultText, warningText } = bannerData;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(
|
||||
config.getPreviewFeatures(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const isEnabled = config.getPreviewFeatures();
|
||||
if (isEnabled !== previewEnabled) {
|
||||
setPreviewEnabled(isEnabled);
|
||||
}
|
||||
}, [config, previewEnabled]);
|
||||
|
||||
const [bannerCounts] = useState(
|
||||
() => persistentState.get('defaultBannerShownCount') || {},
|
||||
);
|
||||
|
||||
const hashedText = crypto
|
||||
.createHash('sha256')
|
||||
.update(defaultText)
|
||||
.digest('hex');
|
||||
|
||||
const currentBannerCount = bannerCounts[hashedText] || 0;
|
||||
|
||||
const showDefaultBanner =
|
||||
warningText === '' &&
|
||||
!previewEnabled &&
|
||||
currentBannerCount < DEFAULT_MAX_BANNER_SHOWN_COUNT;
|
||||
|
||||
const rawBannerText = showDefaultBanner ? defaultText : warningText;
|
||||
const bannerText = rawBannerText.replace(/\\n/g, '\n');
|
||||
|
||||
const lastIncrementedKey = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (showDefaultBanner && defaultText) {
|
||||
if (lastIncrementedKey.current !== defaultText) {
|
||||
lastIncrementedKey.current = defaultText;
|
||||
|
||||
const allCounts = persistentState.get('defaultBannerShownCount') || {};
|
||||
const current = allCounts[hashedText] || 0;
|
||||
|
||||
persistentState.set('defaultBannerShownCount', {
|
||||
...allCounts,
|
||||
[hashedText]: current + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [showDefaultBanner, defaultText, hashedText]);
|
||||
|
||||
return {
|
||||
bannerText,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { writeToStdout } from '../../utils/stdio.js';
|
||||
import { writeToStdout } from '@google/gemini-cli-core';
|
||||
|
||||
const ENABLE_BRACKETED_PASTE = '\x1b[?2004h';
|
||||
const DISABLE_BRACKETED_PASTE = '\x1b[?2004l';
|
||||
|
||||
@@ -136,6 +136,7 @@ export const useGeminiStream = (
|
||||
markToolsAsSubmitted,
|
||||
setToolCallsForDisplay,
|
||||
cancelAllToolCalls,
|
||||
lastToolOutputTime,
|
||||
] = useReactToolScheduler(
|
||||
async (completedToolCallsFromScheduler) => {
|
||||
// This onComplete is called when ALL scheduled tools for a given batch are done.
|
||||
@@ -211,17 +212,18 @@ export const useGeminiStream = (
|
||||
await done;
|
||||
setIsResponding(false);
|
||||
}, []);
|
||||
const { handleShellCommand, activeShellPtyId } = useShellCommandProcessor(
|
||||
addItem,
|
||||
setPendingHistoryItem,
|
||||
onExec,
|
||||
onDebugMessage,
|
||||
config,
|
||||
geminiClient,
|
||||
setShellInputFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
);
|
||||
const { handleShellCommand, activeShellPtyId, lastShellOutputTime } =
|
||||
useShellCommandProcessor(
|
||||
addItem,
|
||||
setPendingHistoryItem,
|
||||
onExec,
|
||||
onDebugMessage,
|
||||
config,
|
||||
geminiClient,
|
||||
setShellInputFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
);
|
||||
|
||||
const activePtyId = activeShellPtyId || activeToolPtyId;
|
||||
|
||||
@@ -681,8 +683,9 @@ export const useGeminiStream = (
|
||||
[FinishReason.UNEXPECTED_TOOL_CALL]:
|
||||
'Response stopped due to unexpected tool call.',
|
||||
[FinishReason.IMAGE_PROHIBITED_CONTENT]:
|
||||
'Response stopped due to prohibited content.',
|
||||
[FinishReason.NO_IMAGE]: 'Response stopped due to no image.',
|
||||
'Response stopped due to prohibited image content.',
|
||||
[FinishReason.NO_IMAGE]:
|
||||
'Response stopped because no image was generated.',
|
||||
};
|
||||
|
||||
const message = finishReasonMessages[finishReason];
|
||||
@@ -1348,6 +1351,8 @@ export const useGeminiStream = (
|
||||
storage,
|
||||
]);
|
||||
|
||||
const lastOutputTime = Math.max(lastToolOutputTime, lastShellOutputTime);
|
||||
|
||||
return {
|
||||
streamingState,
|
||||
submitQuery,
|
||||
@@ -1359,5 +1364,6 @@ export const useGeminiStream = (
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Returns true after a specified delay of inactivity.
|
||||
* Inactivity is defined as 'trigger' not changing for 'delayMs' milliseconds.
|
||||
*
|
||||
* @param isActive Whether the timer should be running.
|
||||
* @param trigger Any value that, when changed, resets the inactivity timer.
|
||||
* @param delayMs The delay in milliseconds before considering the state inactive.
|
||||
*/
|
||||
export const useInactivityTimer = (
|
||||
isActive: boolean,
|
||||
trigger: unknown,
|
||||
delayMs: number = 5000,
|
||||
): boolean => {
|
||||
const [isInactive, setIsInactive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
setIsInactive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsInactive(false);
|
||||
const timer = setTimeout(() => {
|
||||
setIsInactive(true);
|
||||
}, delayMs);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isActive, trigger, delayMs]);
|
||||
|
||||
return isInactive;
|
||||
};
|
||||
@@ -9,8 +9,12 @@ import { act } from 'react';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { useLoadingIndicator } from './useLoadingIndicator.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { PHRASE_CHANGE_INTERVAL_MS } from './usePhraseCycler.js';
|
||||
import {
|
||||
PHRASE_CHANGE_INTERVAL_MS,
|
||||
INTERACTIVE_SHELL_WAITING_PHRASE,
|
||||
} from './usePhraseCycler.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
|
||||
describe('useLoadingIndicator', () => {
|
||||
beforeEach(() => {
|
||||
@@ -25,18 +29,33 @@ describe('useLoadingIndicator', () => {
|
||||
|
||||
const renderLoadingIndicatorHook = (
|
||||
initialStreamingState: StreamingState,
|
||||
initialIsInteractiveShellWaiting: boolean = false,
|
||||
initialLastOutputTime: number = 0,
|
||||
) => {
|
||||
let hookResult: ReturnType<typeof useLoadingIndicator>;
|
||||
function TestComponent({
|
||||
streamingState,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
}: {
|
||||
streamingState: StreamingState;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
}) {
|
||||
hookResult = useLoadingIndicator(streamingState);
|
||||
hookResult = useLoadingIndicator(
|
||||
streamingState,
|
||||
undefined,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const { rerender } = render(
|
||||
<TestComponent streamingState={initialStreamingState} />,
|
||||
<TestComponent
|
||||
streamingState={initialStreamingState}
|
||||
isInteractiveShellWaiting={initialIsInteractiveShellWaiting}
|
||||
lastOutputTime={initialLastOutputTime}
|
||||
/>,
|
||||
);
|
||||
return {
|
||||
result: {
|
||||
@@ -44,8 +63,11 @@ describe('useLoadingIndicator', () => {
|
||||
return hookResult;
|
||||
},
|
||||
},
|
||||
rerender: (newProps: { streamingState: StreamingState }) =>
|
||||
rerender(<TestComponent {...newProps} />),
|
||||
rerender: (newProps: {
|
||||
streamingState: StreamingState;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
}) => rerender(<TestComponent {...newProps} />),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,6 +80,28 @@ describe('useLoadingIndicator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should show interactive shell waiting phrase when isInteractiveShellWaiting is true after 5s', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { result } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
// Initially should be witty phrase or tip
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
result.current.currentLoadingPhrase,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
|
||||
expect(result.current.currentLoadingPhrase).toBe(
|
||||
INTERACTIVE_SHELL_WAITING_PHRASE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reflect values when Responding', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty for subsequent phrases
|
||||
const { result } = renderLoadingIndicatorHook(StreamingState.Responding);
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useState, useEffect, useRef } from 'react'; // Added useRef
|
||||
export const useLoadingIndicator = (
|
||||
streamingState: StreamingState,
|
||||
customWittyPhrases?: string[],
|
||||
isInteractiveShellWaiting: boolean = false,
|
||||
lastOutputTime: number = 0,
|
||||
) => {
|
||||
const [timerResetKey, setTimerResetKey] = useState(0);
|
||||
const isTimerActive = streamingState === StreamingState.Responding;
|
||||
@@ -23,6 +25,8 @@ export const useLoadingIndicator = (
|
||||
const currentLoadingPhrase = usePhraseCycler(
|
||||
isPhraseCyclingActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
customWittyPhrases,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Text } from 'ink';
|
||||
import {
|
||||
usePhraseCycler,
|
||||
PHRASE_CHANGE_INTERVAL_MS,
|
||||
INTERACTIVE_SHELL_WAITING_PHRASE,
|
||||
} from './usePhraseCycler.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
@@ -19,13 +20,23 @@ import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
const TestComponent = ({
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting = false,
|
||||
lastOutputTime = 0,
|
||||
customPhrases,
|
||||
}: {
|
||||
isActive: boolean;
|
||||
isWaiting: boolean;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
customPhrases?: string[];
|
||||
}) => {
|
||||
const phrase = usePhraseCycler(isActive, isWaiting, customPhrases);
|
||||
const phrase = usePhraseCycler(
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
customPhrases,
|
||||
);
|
||||
return <Text>{phrase}</Text>;
|
||||
};
|
||||
|
||||
@@ -57,6 +68,102 @@ describe('usePhraseCycler', () => {
|
||||
expect(lastFrame()).toBe('Waiting for user confirmation...');
|
||||
});
|
||||
|
||||
it('should show interactive shell waiting message when isInteractiveShellWaiting is true after 5s', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { lastFrame, rerender } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
);
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={1}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
// Should still be showing a witty phrase or tip initially
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
lastFrame(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
expect(lastFrame()).toBe(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
});
|
||||
|
||||
it('should reset interactive shell waiting timer when lastOutputTime changes', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
const { lastFrame, rerender } = render(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={1000}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Advance 3 seconds
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
});
|
||||
// Should still be witty phrase or tip
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
lastFrame(),
|
||||
);
|
||||
|
||||
// Update lastOutputTime
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={4000}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Advance another 3 seconds (total 6s from start, but only 3s from last output)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
});
|
||||
// Should STILL be witty phrase or tip because timer reset
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
lastFrame(),
|
||||
);
|
||||
|
||||
// Advance another 2 seconds (total 5s from last output)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
expect(lastFrame()).toBe(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
});
|
||||
|
||||
it('should prioritize interactive shell waiting over normal waiting after 5s', async () => {
|
||||
const { lastFrame, rerender } = render(
|
||||
<TestComponent isActive={true} isWaiting={true} />,
|
||||
);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
expect(lastFrame()).toBe('Waiting for user confirmation...');
|
||||
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={true}
|
||||
isInteractiveShellWaiting={true}
|
||||
lastOutputTime={1}
|
||||
/>,
|
||||
);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
expect(lastFrame()).toBe(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
});
|
||||
|
||||
it('should not cycle phrases if isActive is false and not waiting', async () => {
|
||||
const { lastFrame } = render(
|
||||
<TestComponent isActive={false} isWaiting={false} />,
|
||||
|
||||
@@ -5,20 +5,28 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { SHELL_FOCUS_HINT_DELAY_MS } from '../constants.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import { useInactivityTimer } from './useInactivityTimer.js';
|
||||
|
||||
export const PHRASE_CHANGE_INTERVAL_MS = 15000;
|
||||
export const INTERACTIVE_SHELL_WAITING_PHRASE =
|
||||
'Interactive shell awaiting input... press Ctrl+f to focus shell';
|
||||
|
||||
/**
|
||||
* Custom hook to manage cycling through loading phrases.
|
||||
* @param isActive Whether the phrase cycling should be active.
|
||||
* @param isWaiting Whether to show a specific waiting phrase.
|
||||
* @param isInteractiveShellWaiting Whether an interactive shell is waiting for input but not focused.
|
||||
* @param customPhrases Optional list of custom phrases to use.
|
||||
* @returns The current loading phrase.
|
||||
*/
|
||||
export const usePhraseCycler = (
|
||||
isActive: boolean,
|
||||
isWaiting: boolean,
|
||||
isInteractiveShellWaiting: boolean,
|
||||
lastOutputTime: number = 0,
|
||||
customPhrases?: string[],
|
||||
) => {
|
||||
const loadingPhrases =
|
||||
@@ -29,66 +37,79 @@ export const usePhraseCycler = (
|
||||
const [currentLoadingPhrase, setCurrentLoadingPhrase] = useState(
|
||||
loadingPhrases[0],
|
||||
);
|
||||
const showShellFocusHint = useInactivityTimer(
|
||||
isInteractiveShellWaiting && lastOutputTime > 0,
|
||||
lastOutputTime,
|
||||
SHELL_FOCUS_HINT_DELAY_MS,
|
||||
);
|
||||
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const hasShownFirstRequestTipRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Always clear on re-run
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
|
||||
if (isInteractiveShellWaiting && showShellFocusHint) {
|
||||
setCurrentLoadingPhrase(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWaiting) {
|
||||
setCurrentLoadingPhrase('Waiting for user confirmation...');
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
} else if (isActive) {
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
}
|
||||
|
||||
const setRandomPhrase = () => {
|
||||
if (customPhrases && customPhrases.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * customPhrases.length);
|
||||
setCurrentLoadingPhrase(customPhrases[randomIndex]);
|
||||
} else {
|
||||
let phraseList;
|
||||
// Show a tip on the first request after startup, then continue with 1/6 chance
|
||||
if (!hasShownFirstRequestTipRef.current) {
|
||||
// Show a tip during the first request
|
||||
phraseList = INFORMATIVE_TIPS;
|
||||
hasShownFirstRequestTipRef.current = true;
|
||||
} else {
|
||||
// Roughly 1 in 6 chance to show a tip after the first request
|
||||
const showTip = Math.random() < 1 / 6;
|
||||
phraseList = showTip ? INFORMATIVE_TIPS : WITTY_LOADING_PHRASES;
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * phraseList.length);
|
||||
setCurrentLoadingPhrase(phraseList[randomIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
// Select an initial random phrase
|
||||
setRandomPhrase();
|
||||
|
||||
phraseIntervalRef.current = setInterval(() => {
|
||||
// Select a new random phrase
|
||||
setRandomPhrase();
|
||||
}, PHRASE_CHANGE_INTERVAL_MS);
|
||||
} else {
|
||||
// Idle or other states, clear the phrase interval
|
||||
// and reset to the first phrase for next active state.
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
setCurrentLoadingPhrase(loadingPhrases[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
setCurrentLoadingPhrase(loadingPhrases[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const setRandomPhrase = () => {
|
||||
if (customPhrases && customPhrases.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * customPhrases.length);
|
||||
setCurrentLoadingPhrase(customPhrases[randomIndex]);
|
||||
} else {
|
||||
let phraseList;
|
||||
// Show a tip on the first request after startup, then continue with 1/6 chance
|
||||
if (!hasShownFirstRequestTipRef.current) {
|
||||
// Show a tip during the first request
|
||||
phraseList = INFORMATIVE_TIPS;
|
||||
hasShownFirstRequestTipRef.current = true;
|
||||
} else {
|
||||
// Roughly 1 in 6 chance to show a tip after the first request
|
||||
const showTip = Math.random() < 1 / 6;
|
||||
phraseList = showTip ? INFORMATIVE_TIPS : WITTY_LOADING_PHRASES;
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * phraseList.length);
|
||||
setCurrentLoadingPhrase(phraseList[randomIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
// Select an initial random phrase
|
||||
setRandomPhrase();
|
||||
|
||||
phraseIntervalRef.current = setInterval(() => {
|
||||
// Select a new random phrase
|
||||
setRandomPhrase();
|
||||
}, PHRASE_CHANGE_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
if (phraseIntervalRef.current) {
|
||||
clearInterval(phraseIntervalRef.current);
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isActive, isWaiting, customPhrases, loadingPhrases]);
|
||||
}, [
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
customPhrases,
|
||||
loadingPhrases,
|
||||
showShellFocusHint,
|
||||
]);
|
||||
|
||||
return currentLoadingPhrase;
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
RetryableQuotaError,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
ModelNotFoundError,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useQuotaAndFallback } from './useQuotaAndFallback.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -432,7 +434,7 @@ To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
'gemini-flash',
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
new Error('preview model failed'),
|
||||
);
|
||||
});
|
||||
@@ -447,7 +449,42 @@ To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain(
|
||||
`Switched to fallback model gemini-flash. We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.`,
|
||||
`Switched to fallback model gemini-2.5-pro. We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should show a special message when falling back from the preview model, but do not show periodical check message for flash model fallback', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
await act(() => {
|
||||
promise = handler(
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
new Error('preview model failed'),
|
||||
);
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
result.current.handleProQuotaChoice('retry_always');
|
||||
});
|
||||
|
||||
await promise!;
|
||||
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain(
|
||||
`Switched to fallback model gemini-2.5-flash.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ModelNotFoundError,
|
||||
type UserTierId,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -55,11 +56,16 @@ export function useQuotaAndFallback({
|
||||
let message: string;
|
||||
let isTerminalQuotaError = false;
|
||||
let isModelNotFoundError = false;
|
||||
const usageLimitReachedModel =
|
||||
failedModel === DEFAULT_GEMINI_MODEL ||
|
||||
failedModel === PREVIEW_GEMINI_MODEL
|
||||
? 'all Pro models'
|
||||
: failedModel;
|
||||
if (error instanceof TerminalQuotaError) {
|
||||
isTerminalQuotaError = true;
|
||||
// Common part of the message for both tiers
|
||||
const messageLines = [
|
||||
`Usage limit reached for ${failedModel}.`,
|
||||
`Usage limit reached for ${usageLimitReachedModel}.`,
|
||||
error.retryDelayMs ? getResetTimeMessage(error.retryDelayMs) : null,
|
||||
`/stats for usage details`,
|
||||
`/auth to switch to API key.`,
|
||||
@@ -116,10 +122,13 @@ export function useQuotaAndFallback({
|
||||
if (choice === 'retry_always') {
|
||||
// If we were recovering from a Preview Model failure, show a specific message.
|
||||
if (proQuotaRequest.failedModel === PREVIEW_GEMINI_MODEL) {
|
||||
const showPeriodicalCheckMessage =
|
||||
!proQuotaRequest.isModelNotFoundError &&
|
||||
proQuotaRequest.fallbackModel === DEFAULT_GEMINI_MODEL;
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Switched to fallback model ${proQuotaRequest.fallbackModel}. ${!proQuotaRequest.isModelNotFoundError ? `We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.` : ''}`,
|
||||
text: `Switched to fallback model ${proQuotaRequest.fallbackModel}. ${showPeriodicalCheckMessage ? `We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.` : ''}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -74,10 +74,12 @@ export function useReactToolScheduler(
|
||||
MarkToolsAsSubmittedFn,
|
||||
React.Dispatch<React.SetStateAction<TrackedToolCall[]>>,
|
||||
CancelAllFn,
|
||||
number,
|
||||
] {
|
||||
const [toolCallsForDisplay, setToolCallsForDisplay] = useState<
|
||||
TrackedToolCall[]
|
||||
>([]);
|
||||
const [lastToolOutputTime, setLastToolOutputTime] = useState<number>(0);
|
||||
|
||||
// Store callbacks in refs to keep them up-to-date without causing re-renders.
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
@@ -93,6 +95,7 @@ export function useReactToolScheduler(
|
||||
|
||||
const outputUpdateHandler: OutputUpdateHandler = useCallback(
|
||||
(toolCallId, outputChunk) => {
|
||||
setLastToolOutputTime(Date.now());
|
||||
setToolCallsForDisplay((prevCalls) =>
|
||||
prevCalls.map((tc) => {
|
||||
if (tc.request.callId === toolCallId && tc.status === 'executing') {
|
||||
@@ -208,6 +211,7 @@ export function useReactToolScheduler(
|
||||
markToolsAsSubmitted,
|
||||
setToolCallsForDisplay,
|
||||
cancelAllToolCalls,
|
||||
lastToolOutputTime,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,11 @@ export async function detectAndEnableKittyProtocol(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
import {
|
||||
enableKittyKeyboardProtocol,
|
||||
disableKittyKeyboardProtocol,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export function isKittyProtocolEnabled(): boolean {
|
||||
return kittyEnabled;
|
||||
}
|
||||
@@ -104,8 +109,7 @@ export function isKittyProtocolEnabled(): boolean {
|
||||
function disableAllProtocols() {
|
||||
try {
|
||||
if (kittyEnabled) {
|
||||
// use writeSync to avoid race conditions
|
||||
fs.writeSync(process.stdout.fd, '\x1b[<u');
|
||||
disableKittyKeyboardProtocol();
|
||||
kittyEnabled = false;
|
||||
}
|
||||
} catch {
|
||||
@@ -120,8 +124,7 @@ function disableAllProtocols() {
|
||||
export function enableSupportedProtocol(): void {
|
||||
try {
|
||||
if (kittySupported) {
|
||||
// use writeSync to avoid race conditions
|
||||
fs.writeSync(process.stdout.fd, '\x1b[>1u');
|
||||
enableKittyKeyboardProtocol();
|
||||
kittyEnabled = true;
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { writeToStdout } from '../../utils/stdio.js';
|
||||
import { enableMouseEvents, disableMouseEvents } from '@google/gemini-cli-core';
|
||||
import {
|
||||
SGR_MOUSE_REGEX,
|
||||
X11_MOUSE_REGEX,
|
||||
@@ -230,14 +230,4 @@ export function isIncompleteMouseSequence(buffer: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function enableMouseEvents() {
|
||||
// Enable mouse tracking with SGR format
|
||||
// ?1002h = button event tracking (clicks + drags + scroll wheel)
|
||||
// ?1006h = SGR extended mouse mode (better coordinate handling)
|
||||
writeToStdout('\u001b[?1002h\u001b[?1006h');
|
||||
}
|
||||
|
||||
export function disableMouseEvents() {
|
||||
// Disable mouse tracking with SGR format
|
||||
writeToStdout('\u001b[?1006l\u001b[?1002l');
|
||||
}
|
||||
export { enableMouseEvents, disableMouseEvents };
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as path from 'node:path';
|
||||
const STATE_FILENAME = 'state.json';
|
||||
|
||||
interface PersistentStateData {
|
||||
defaultBannerShownCount?: number;
|
||||
defaultBannerShownCount?: Record<string, number>;
|
||||
// Add other persistent state keys here as needed
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"description": "Gemini CLI Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,7 +26,7 @@
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.3",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
|
||||
|
||||
@@ -27,6 +27,7 @@ import readline from 'node:readline';
|
||||
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
|
||||
import { GEMINI_DIR } from '../utils/paths.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { writeToStdout } from '../utils/stdio.js';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const os = await importOriginal<typeof import('os')>();
|
||||
@@ -44,6 +45,19 @@ vi.mock('node:readline');
|
||||
vi.mock('../utils/browser.js', () => ({
|
||||
shouldAttemptBrowserLaunch: () => true,
|
||||
}));
|
||||
vi.mock('../utils/stdio.js', () => ({
|
||||
writeToStdout: vi.fn(),
|
||||
writeToStderr: vi.fn(),
|
||||
createInkStdio: vi.fn(() => ({
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
})),
|
||||
enterAlternateScreen: vi.fn(),
|
||||
exitAlternateScreen: vi.fn(),
|
||||
enableLineWrapping: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
disableKittyKeyboardProtocol: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./oauth-credential-storage.js', () => ({
|
||||
OAuthCredentialStorage: {
|
||||
@@ -238,13 +252,10 @@ describe('oauth2', () => {
|
||||
const mockReadline = {
|
||||
question: vi.fn((_query, callback) => callback(mockCode)),
|
||||
close: vi.fn(),
|
||||
on: vi.fn(),
|
||||
};
|
||||
(readline.createInterface as Mock).mockReturnValue(mockReadline);
|
||||
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const client = await getOauthClient(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
mockConfigWithNoBrowser,
|
||||
@@ -255,7 +266,7 @@ describe('oauth2', () => {
|
||||
// Verify the auth flow
|
||||
expect(mockGenerateCodeVerifierAsync).toHaveBeenCalled();
|
||||
expect(mockGenerateAuthUrl).toHaveBeenCalled();
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(writeToStdout)).toHaveBeenCalledWith(
|
||||
expect.stringContaining(mockAuthUrl),
|
||||
);
|
||||
expect(mockReadline.question).toHaveBeenCalledWith(
|
||||
@@ -268,8 +279,6 @@ describe('oauth2', () => {
|
||||
redirect_uri: 'https://codeassist.google.com/authcode',
|
||||
});
|
||||
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('in Cloud Shell', () => {
|
||||
@@ -932,6 +941,7 @@ describe('oauth2', () => {
|
||||
const mockReadline = {
|
||||
question: vi.fn((_query, callback) => callback('invalid-code')),
|
||||
close: vi.fn(),
|
||||
on: vi.fn(),
|
||||
};
|
||||
(readline.createInterface as Mock).mockReturnValue(mockReadline);
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ import open from 'open';
|
||||
import path from 'node:path';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { getErrorMessage, FatalAuthenticationError } from '../utils/errors.js';
|
||||
import {
|
||||
getErrorMessage,
|
||||
FatalAuthenticationError,
|
||||
FatalCancellationError,
|
||||
} from '../utils/errors.js';
|
||||
import { UserAccountManager } from '../utils/userAccountManager.js';
|
||||
import { AuthType } from '../core/contentGenerator.js';
|
||||
import readline from 'node:readline';
|
||||
@@ -27,6 +31,19 @@ import { Storage } from '../config/storage.js';
|
||||
import { OAuthCredentialStorage } from './oauth-credential-storage.js';
|
||||
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import {
|
||||
writeToStdout,
|
||||
createInkStdio,
|
||||
writeToStderr,
|
||||
} from '../utils/stdio.js';
|
||||
import {
|
||||
enableLineWrapping,
|
||||
disableMouseEvents,
|
||||
disableKittyKeyboardProtocol,
|
||||
enterAlternateScreen,
|
||||
exitAlternateScreen,
|
||||
} from '../utils/terminal.js';
|
||||
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||
|
||||
const userAccountManager = new UserAccountManager();
|
||||
|
||||
@@ -185,16 +202,34 @@ async function initOauthClient(
|
||||
if (config.isBrowserLaunchSuppressed()) {
|
||||
let success = false;
|
||||
const maxRetries = 2;
|
||||
for (let i = 0; !success && i < maxRetries; i++) {
|
||||
success = await authWithUserCode(client);
|
||||
if (!success) {
|
||||
debugLogger.error(
|
||||
'\nFailed to authenticate with user code.',
|
||||
i === maxRetries - 1 ? '' : 'Retrying...\n',
|
||||
);
|
||||
// Enter alternate buffer
|
||||
enterAlternateScreen();
|
||||
// Clear screen and move cursor to top-left.
|
||||
writeToStdout('\u001B[2J\u001B[H');
|
||||
disableMouseEvents();
|
||||
disableKittyKeyboardProtocol();
|
||||
enableLineWrapping();
|
||||
|
||||
try {
|
||||
for (let i = 0; !success && i < maxRetries; i++) {
|
||||
success = await authWithUserCode(client);
|
||||
if (!success) {
|
||||
writeToStderr(
|
||||
'\nFailed to authenticate with user code.' +
|
||||
(i === maxRetries - 1 ? '' : ' Retrying...\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
exitAlternateScreen();
|
||||
// If this was triggered from an active Gemini CLI TUI this event ensures
|
||||
// the TUI will re-initialize the terminal state just like it will when
|
||||
// another editor like VIM may have modified the buffer of settings.
|
||||
coreEvents.emit(CoreEvent.ExternalEditorClosed);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
writeToStderr('Failed to authenticate with user code.\n');
|
||||
throw new FatalAuthenticationError(
|
||||
'Failed to authenticate with user code.',
|
||||
);
|
||||
@@ -202,11 +237,13 @@ async function initOauthClient(
|
||||
} else {
|
||||
const webLogin = await authWithWeb(client);
|
||||
|
||||
debugLogger.log(
|
||||
`\n\nCode Assist login required.\n` +
|
||||
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||
severity: 'info',
|
||||
message:
|
||||
`\n\nCode Assist login required.\n` +
|
||||
`Attempting to open authentication page in your browser.\n` +
|
||||
`Otherwise navigate to:\n\n${webLogin.authUrl}\n\n`,
|
||||
);
|
||||
`Otherwise navigate to:\n\n${webLogin.authUrl}\n\n\n`,
|
||||
});
|
||||
try {
|
||||
// Attempt to open the authentication URL in the default browser.
|
||||
// We do not use the `wait` option here because the main script's execution
|
||||
@@ -218,23 +255,28 @@ async function initOauthClient(
|
||||
// in a minimal Docker container), it will emit an unhandled 'error' event,
|
||||
// causing the entire Node.js process to crash.
|
||||
childProcess.on('error', (error) => {
|
||||
debugLogger.error(
|
||||
`Failed to open browser with error:`,
|
||||
getErrorMessage(error),
|
||||
`\nPlease try running again with NO_BROWSER=true set.`,
|
||||
);
|
||||
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||
severity: 'error',
|
||||
message:
|
||||
`Failed to open browser with error: ${getErrorMessage(error)}\n` +
|
||||
`Please try running again with NO_BROWSER=true set.`,
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
debugLogger.error(
|
||||
`Failed to open browser with error:`,
|
||||
getErrorMessage(err),
|
||||
`\nPlease try running again with NO_BROWSER=true set.`,
|
||||
);
|
||||
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||
severity: 'error',
|
||||
message:
|
||||
`Failed to open browser with error: ${getErrorMessage(err)}\n` +
|
||||
`Please try running again with NO_BROWSER=true set.`,
|
||||
});
|
||||
throw new FatalAuthenticationError(
|
||||
`Failed to open browser: ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
debugLogger.log('Waiting for authentication...');
|
||||
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||
severity: 'info',
|
||||
message: 'Waiting for authentication...\n',
|
||||
});
|
||||
|
||||
// Add timeout to prevent infinite waiting when browser tab gets stuck
|
||||
const authTimeout = 5 * 60 * 1000; // 5 minutes timeout
|
||||
@@ -250,6 +292,11 @@ async function initOauthClient(
|
||||
});
|
||||
|
||||
await Promise.race([webLogin.loginCompletePromise, timeoutPromise]);
|
||||
|
||||
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||
severity: 'info',
|
||||
message: 'Authentication succeeded\n',
|
||||
});
|
||||
}
|
||||
|
||||
return client;
|
||||
@@ -266,55 +313,77 @@ export async function getOauthClient(
|
||||
}
|
||||
|
||||
async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
|
||||
const redirectUri = 'https://codeassist.google.com/authcode';
|
||||
const codeVerifier = await client.generateCodeVerifierAsync();
|
||||
const state = crypto.randomBytes(32).toString('hex');
|
||||
const authUrl: string = client.generateAuthUrl({
|
||||
redirect_uri: redirectUri,
|
||||
access_type: 'offline',
|
||||
scope: OAUTH_SCOPE,
|
||||
code_challenge_method: CodeChallengeMethod.S256,
|
||||
code_challenge: codeVerifier.codeChallenge,
|
||||
state,
|
||||
});
|
||||
debugLogger.log(
|
||||
'Please visit the following URL to authorize the application:',
|
||||
);
|
||||
debugLogger.log('');
|
||||
debugLogger.log(authUrl);
|
||||
debugLogger.log('');
|
||||
|
||||
const code = await new Promise<string>((resolve) => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
rl.question('Enter the authorization code: ', (code) => {
|
||||
rl.close();
|
||||
resolve(code.trim());
|
||||
});
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
debugLogger.error('Authorization code is required.');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { tokens } = await client.getToken({
|
||||
code,
|
||||
codeVerifier: codeVerifier.codeVerifier,
|
||||
const redirectUri = 'https://codeassist.google.com/authcode';
|
||||
const codeVerifier = await client.generateCodeVerifierAsync();
|
||||
const state = crypto.randomBytes(32).toString('hex');
|
||||
const authUrl: string = client.generateAuthUrl({
|
||||
redirect_uri: redirectUri,
|
||||
access_type: 'offline',
|
||||
scope: OAUTH_SCOPE,
|
||||
code_challenge_method: CodeChallengeMethod.S256,
|
||||
code_challenge: codeVerifier.codeChallenge,
|
||||
state,
|
||||
});
|
||||
client.setCredentials(tokens);
|
||||
} catch (error) {
|
||||
writeToStdout(
|
||||
'Please visit the following URL to authorize the application:\n\n' +
|
||||
authUrl +
|
||||
'\n\n',
|
||||
);
|
||||
|
||||
const code = await new Promise<string>((resolve, _) => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: createInkStdio().stdout,
|
||||
terminal: true,
|
||||
});
|
||||
|
||||
rl.question('Enter the authorization code: ', (code) => {
|
||||
rl.close();
|
||||
resolve(code.trim());
|
||||
});
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
writeToStderr('Authorization code is required.\n');
|
||||
debugLogger.error('Authorization code is required.');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { tokens } = await client.getToken({
|
||||
code,
|
||||
codeVerifier: codeVerifier.codeVerifier,
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
client.setCredentials(tokens);
|
||||
} catch (error) {
|
||||
writeToStderr(
|
||||
'Failed to authenticate with authorization code:' +
|
||||
getErrorMessage(error) +
|
||||
'\n',
|
||||
);
|
||||
|
||||
debugLogger.error(
|
||||
'Failed to authenticate with authorization code:',
|
||||
getErrorMessage(error),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof FatalCancellationError) {
|
||||
throw err;
|
||||
}
|
||||
writeToStderr(
|
||||
'Failed to authenticate with user code:' + getErrorMessage(err) + '\n',
|
||||
);
|
||||
debugLogger.error(
|
||||
'Failed to authenticate with authorization code:',
|
||||
getErrorMessage(error),
|
||||
'Failed to authenticate with user code:',
|
||||
getErrorMessage(err),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function authWithWeb(client: OAuth2Client): Promise<OauthWebLogin> {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { setSimulate429 } from '../utils/testUtils.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_THINKING_MODE,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
} from '../config/models.js';
|
||||
import { AuthType } from './contentGenerator.js';
|
||||
@@ -131,15 +132,22 @@ describe('GeminiChat', () => {
|
||||
getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
|
||||
getRetryFetchErrors: vi.fn().mockReturnValue(false),
|
||||
modelConfigService: {
|
||||
getResolvedConfig: vi.fn().mockImplementation((modelConfigKey) => ({
|
||||
model: modelConfigKey.model,
|
||||
generateContentConfig: {
|
||||
temperature: 0,
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 1000,
|
||||
getResolvedConfig: vi.fn().mockImplementation((modelConfigKey) => {
|
||||
const thinkingConfig = modelConfigKey.model.startsWith('gemini-3')
|
||||
? {
|
||||
thinkingLevel: ThinkingLevel.HIGH,
|
||||
}
|
||||
: {
|
||||
thinkingBudget: DEFAULT_THINKING_MODE,
|
||||
};
|
||||
return {
|
||||
model: modelConfigKey.model,
|
||||
generateContentConfig: {
|
||||
temperature: 0,
|
||||
thinkingConfig,
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}),
|
||||
},
|
||||
isPreviewModelBypassMode: vi.fn().mockReturnValue(false),
|
||||
setPreviewModelBypassMode: vi.fn(),
|
||||
@@ -976,7 +984,7 @@ describe('GeminiChat', () => {
|
||||
tools: [],
|
||||
temperature: 0,
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 1000,
|
||||
thinkingBudget: DEFAULT_THINKING_MODE,
|
||||
},
|
||||
abortSignal: expect.any(AbortSignal),
|
||||
},
|
||||
@@ -1023,6 +1031,45 @@ describe('GeminiChat', () => {
|
||||
'prompt-id-thinking-level',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use thinkingBudget and remove thinkingLevel for non-gemini-3 models', async () => {
|
||||
const response = (async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{
|
||||
content: { parts: [{ text: 'response' }], role: 'model' },
|
||||
finishReason: 'STOP',
|
||||
},
|
||||
],
|
||||
} as unknown as GenerateContentResponse;
|
||||
})();
|
||||
vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(
|
||||
response,
|
||||
);
|
||||
|
||||
const stream = await chat.sendMessageStream(
|
||||
{ model: 'gemini-2.0-flash' },
|
||||
'hello',
|
||||
'prompt-id-thinking-budget',
|
||||
new AbortController().signal,
|
||||
);
|
||||
for await (const _ of stream) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'gemini-2.0-flash',
|
||||
config: expect.objectContaining({
|
||||
thinkingConfig: {
|
||||
thinkingBudget: DEFAULT_THINKING_MODE,
|
||||
thinkingLevel: undefined,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'prompt-id-thinking-budget',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addHistory', () => {
|
||||
@@ -1902,6 +1949,92 @@ describe('GeminiChat', () => {
|
||||
expect(modelTurn.parts![0]!.text).toBe('Success on retry');
|
||||
});
|
||||
|
||||
it('should switch to DEFAULT_GEMINI_FLASH_MODEL and use thinkingBudget when falling back from a gemini-3 model', async () => {
|
||||
// ARRANGE
|
||||
const authType = AuthType.LOGIN_WITH_GOOGLE;
|
||||
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||
authType,
|
||||
});
|
||||
|
||||
// Initial state: Not in fallback mode
|
||||
const isInFallbackModeSpy = vi.spyOn(mockConfig, 'isInFallbackMode');
|
||||
isInFallbackModeSpy.mockReturnValue(false);
|
||||
|
||||
// Mock API calls:
|
||||
// 1. Fails with 429 (simulating gemini-3 failure)
|
||||
// 2. Succeeds (simulating fallback success)
|
||||
vi.mocked(mockContentGenerator.generateContentStream)
|
||||
.mockRejectedValueOnce(error429)
|
||||
.mockResolvedValueOnce(
|
||||
(async function* () {
|
||||
yield {
|
||||
candidates: [
|
||||
{
|
||||
content: { parts: [{ text: 'Fallback success' }] },
|
||||
finishReason: 'STOP',
|
||||
},
|
||||
],
|
||||
} as unknown as GenerateContentResponse;
|
||||
})(),
|
||||
);
|
||||
|
||||
// Mock handleFallback to enable fallback mode and signal retry
|
||||
mockHandleFallback.mockImplementation(async () => {
|
||||
isInFallbackModeSpy.mockReturnValue(true); // Next call will see fallback mode = true
|
||||
return true;
|
||||
});
|
||||
|
||||
// ACT
|
||||
const stream = await chat.sendMessageStream(
|
||||
{ model: 'gemini-3-test-model' }, // Start with a gemini-3 model
|
||||
'test fallback thinking',
|
||||
'prompt-id-fb3',
|
||||
new AbortController().signal,
|
||||
);
|
||||
for await (const _ of stream) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
// ASSERT
|
||||
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
|
||||
// First call: gemini-3 model, thinkingLevel set
|
||||
expect(
|
||||
mockContentGenerator.generateContentStream,
|
||||
).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
model: 'gemini-3-test-model',
|
||||
config: expect.objectContaining({
|
||||
thinkingConfig: {
|
||||
thinkingBudget: undefined,
|
||||
thinkingLevel: ThinkingLevel.HIGH,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'prompt-id-fb3',
|
||||
);
|
||||
|
||||
// Second call: DEFAULT_GEMINI_FLASH_MODEL (due to fallback), thinkingBudget set (due to fix)
|
||||
expect(
|
||||
mockContentGenerator.generateContentStream,
|
||||
).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
model: DEFAULT_GEMINI_FLASH_MODEL,
|
||||
config: expect.objectContaining({
|
||||
thinkingConfig: {
|
||||
thinkingBudget: DEFAULT_THINKING_MODE,
|
||||
thinkingLevel: undefined,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'prompt-id-fb3',
|
||||
);
|
||||
});
|
||||
|
||||
it('should stop retrying if handleFallback returns false (e.g., auth intent)', async () => {
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue('gemini-pro');
|
||||
vi.mocked(mockContentGenerator.generateContentStream).mockRejectedValue(
|
||||
|
||||
@@ -22,6 +22,7 @@ import { retryWithBackoff } from '../utils/retry.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_THINKING_MODE,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
getEffectiveModel,
|
||||
isGemini2Model,
|
||||
@@ -428,6 +429,14 @@ export class GeminiChat {
|
||||
thinkingLevel: ThinkingLevel.HIGH,
|
||||
};
|
||||
delete config.thinkingConfig?.thinkingBudget;
|
||||
} else {
|
||||
// The `gemini-3` configs use thinkingLevel, so we have to invert the
|
||||
// change above.
|
||||
config.thinkingConfig = {
|
||||
...config.thinkingConfig,
|
||||
thinkingBudget: DEFAULT_THINKING_MODE,
|
||||
};
|
||||
delete config.thinkingConfig?.thinkingLevel;
|
||||
}
|
||||
|
||||
return this.config.getContentGenerator().generateContentStream(
|
||||
|
||||
@@ -25,6 +25,10 @@ import {
|
||||
import { logFlashFallback } from '../telemetry/index.js';
|
||||
import type { FallbackModelHandler } from './types.js';
|
||||
import { ModelNotFoundError } from '../utils/httpErrors.js';
|
||||
import {
|
||||
RetryableQuotaError,
|
||||
TerminalQuotaError,
|
||||
} from '../utils/googleQuotaErrors.js';
|
||||
|
||||
// Mock the telemetry logger and event class
|
||||
vi.mock('../telemetry/index.js', () => ({
|
||||
@@ -104,7 +108,7 @@ describe('handleFallback', () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
describe('when handler returns "retry"', () => {
|
||||
describe('when handler returns "retry_always"', () => {
|
||||
it('should activate fallback mode, log telemetry, and return true', async () => {
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
@@ -212,65 +216,175 @@ describe('handleFallback', () => {
|
||||
describe('Preview Model Fallback Logic', () => {
|
||||
const previewModel = PREVIEW_GEMINI_MODEL;
|
||||
|
||||
it('should always set Preview Model bypass mode on failure', async () => {
|
||||
await handleFallback(mockConfig, previewModel, AUTH_OAUTH);
|
||||
it('should only set Preview Model bypass mode on retryable quota failure', async () => {
|
||||
const mockGoogleApiError = {
|
||||
code: 429,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const retryableQuotaError = new RetryableQuotaError(
|
||||
'Capacity error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
await handleFallback(
|
||||
mockConfig,
|
||||
previewModel,
|
||||
AUTH_OAUTH,
|
||||
retryableQuotaError,
|
||||
);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should silently retry if Preview Model fallback mode is already active', async () => {
|
||||
vi.spyOn(mockConfig, 'isPreviewModelFallbackMode').mockReturnValue(true);
|
||||
it('should not set Preview Model bypass mode on non-retryable quota failure', async () => {
|
||||
const mockGoogleApiError = {
|
||||
code: 429,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const terminalQuotaError = new TerminalQuotaError(
|
||||
'quota error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
await handleFallback(
|
||||
mockConfig,
|
||||
previewModel,
|
||||
AUTH_OAUTH,
|
||||
terminalQuotaError,
|
||||
);
|
||||
|
||||
const result = await handleFallback(mockConfig, previewModel, AUTH_OAUTH);
|
||||
expect(mockConfig.setPreviewModelBypassMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should silently retry if Preview Model fallback mode is already active and error is retryable error', async () => {
|
||||
vi.spyOn(mockConfig, 'isPreviewModelFallbackMode').mockReturnValue(true);
|
||||
const mockGoogleApiError = {
|
||||
code: 429,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const retryableQuotaError = new RetryableQuotaError(
|
||||
'Capacity error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
previewModel,
|
||||
AUTH_OAUTH,
|
||||
retryableQuotaError,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should activate Preview Model fallback mode when handler returns "retry_always"', async () => {
|
||||
it('should activate Preview Model fallback mode when handler returns "retry_always" and is RetryableQuotaError', async () => {
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
const result = await handleFallback(mockConfig, previewModel, AUTH_OAUTH);
|
||||
const mockGoogleApiError = {
|
||||
code: 429,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const retryableQuotaError = new RetryableQuotaError(
|
||||
'Capacity error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
previewModel,
|
||||
AUTH_OAUTH,
|
||||
retryableQuotaError,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
expect(mockConfig.setPreviewModelFallbackMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should activate regular fallback when handler returns "retry_always" and is TerminalQuotaError', async () => {
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
const mockGoogleApiError = {
|
||||
code: 503,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const terminalError = new TerminalQuotaError(
|
||||
'Quota error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
previewModel,
|
||||
AUTH_OAUTH,
|
||||
terminalError,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockConfig.setPreviewModelFallbackMode).not.toBeCalled();
|
||||
expect(mockConfig.setFallbackMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should NOT set fallback mode if user chooses "retry_once"', async () => {
|
||||
const mockGoogleApiError = {
|
||||
code: 429,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const terminalQuotaError = new TerminalQuotaError(
|
||||
'quota error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
mockHandler.mockResolvedValue('retry_once');
|
||||
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
new Error('Capacity'),
|
||||
terminalQuotaError,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
expect(mockConfig.setPreviewModelBypassMode).not.toHaveBeenCalled();
|
||||
expect(mockConfig.setPreviewModelFallbackMode).not.toHaveBeenCalled();
|
||||
expect(mockConfig.setFallbackMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set fallback mode if user chooses "retry_always"', async () => {
|
||||
mockHandler.mockResolvedValue('retry_always');
|
||||
|
||||
const result = await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
new Error('Capacity'),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockConfig.setPreviewModelBypassMode).toHaveBeenCalledWith(true);
|
||||
expect(mockConfig.setPreviewModelFallbackMode).toHaveBeenCalledWith(true);
|
||||
});
|
||||
it('should pass DEFAULT_GEMINI_MODEL as fallback when Preview Model fails', async () => {
|
||||
it('should pass DEFAULT_GEMINI_MODEL as fallback when Preview Model fails with Retryable Error', async () => {
|
||||
const mockFallbackHandler = vi.fn().mockResolvedValue('stop');
|
||||
vi.mocked(mockConfig.fallbackModelHandler!).mockImplementation(
|
||||
mockFallbackHandler,
|
||||
);
|
||||
const mockGoogleApiError = {
|
||||
code: 429,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const retryableQuotaError = new RetryableQuotaError(
|
||||
'Capacity error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
|
||||
await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
retryableQuotaError,
|
||||
);
|
||||
|
||||
expect(mockConfig.fallbackModelHandler).toHaveBeenCalledWith(
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
retryableQuotaError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass DEFAULT_GEMINI_MODEL as fallback when Preview Model fails with other error', async () => {
|
||||
await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
@@ -283,6 +397,31 @@ describe('handleFallback', () => {
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass DEFAULT_GEMINI_FLASH_MODEL as fallback when Preview Model fails with other error', async () => {
|
||||
const mockGoogleApiError = {
|
||||
code: 429,
|
||||
message: 'mock error',
|
||||
details: [],
|
||||
};
|
||||
const terminalQuotaError = new TerminalQuotaError(
|
||||
'quota error',
|
||||
mockGoogleApiError,
|
||||
5,
|
||||
);
|
||||
await handleFallback(
|
||||
mockConfig,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
terminalQuotaError,
|
||||
);
|
||||
|
||||
expect(mockConfig.fallbackModelHandler).toHaveBeenCalledWith(
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
terminalQuotaError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if ModelNotFoundError occurs for a non-preview model', async () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
import { ModelNotFoundError } from '../utils/httpErrors.js';
|
||||
import { TerminalQuotaError } from '../utils/googleQuotaErrors.js';
|
||||
|
||||
const UPGRADE_URL_PAGE = 'https://goo.gle/set-up-gemini-code-assist';
|
||||
|
||||
@@ -36,10 +37,12 @@ export async function handleFallback(
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shouldActivatePreviewFallback =
|
||||
failedModel === PREVIEW_GEMINI_MODEL &&
|
||||
!(error instanceof TerminalQuotaError);
|
||||
// Preview Model Specific Logic
|
||||
if (failedModel === PREVIEW_GEMINI_MODEL) {
|
||||
// Always set bypass mode for the immediate retry.
|
||||
if (shouldActivatePreviewFallback) {
|
||||
// Always set bypass mode for the immediate retry, for non-TerminalQuotaErrors.
|
||||
// This ensures the next attempt uses 2.5 Pro.
|
||||
config.setPreviewModelBypassMode(true);
|
||||
|
||||
@@ -50,10 +53,9 @@ export async function handleFallback(
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackModel =
|
||||
failedModel === PREVIEW_GEMINI_MODEL
|
||||
? DEFAULT_GEMINI_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
const fallbackModel = shouldActivatePreviewFallback
|
||||
? DEFAULT_GEMINI_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
|
||||
// Consult UI Handler for Intent
|
||||
const fallbackModelHandler = config.fallbackModelHandler;
|
||||
@@ -70,7 +72,9 @@ export async function handleFallback(
|
||||
// Process Intent and Update State
|
||||
switch (intent) {
|
||||
case 'retry_always':
|
||||
if (failedModel === PREVIEW_GEMINI_MODEL) {
|
||||
// If the error is non-retryable, e.g. TerminalQuota Error, trigger a regular fallback to flash.
|
||||
// For all other errors, activate previewModel fallback.
|
||||
if (shouldActivatePreviewFallback) {
|
||||
activatePreviewModelFallbackMode(config);
|
||||
} else {
|
||||
activateFallbackMode(config, authType);
|
||||
|
||||
@@ -144,3 +144,7 @@ export * from './test-utils/index.js';
|
||||
|
||||
// Export hook types
|
||||
export * from './hooks/types.js';
|
||||
|
||||
// Export stdio utils
|
||||
export * from './utils/stdio.js';
|
||||
export * from './utils/terminal.js';
|
||||
|
||||
@@ -487,15 +487,17 @@ describe('mcp-client', () => {
|
||||
false,
|
||||
);
|
||||
|
||||
expect(transport).toEqual(
|
||||
new StreamableHTTPClientTransport(new URL('http://test-server'), {
|
||||
requestInit: { headers: {} },
|
||||
}),
|
||||
expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
|
||||
expect(transport).toHaveProperty(
|
||||
'_url',
|
||||
new URL('http://test-server/'),
|
||||
);
|
||||
});
|
||||
|
||||
it('with headers', async () => {
|
||||
const transport = await createTransport(
|
||||
// We need this to be an any type because we dig into its private state.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const transport: any = await createTransport(
|
||||
'test-server',
|
||||
{
|
||||
httpUrl: 'http://test-server',
|
||||
@@ -503,14 +505,13 @@ describe('mcp-client', () => {
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
expect(transport).toEqual(
|
||||
new StreamableHTTPClientTransport(new URL('http://test-server'), {
|
||||
requestInit: {
|
||||
headers: { Authorization: 'derp' },
|
||||
},
|
||||
}),
|
||||
expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
|
||||
expect(transport).toHaveProperty(
|
||||
'_url',
|
||||
new URL('http://test-server/'),
|
||||
);
|
||||
const authHeader = transport._requestInit?.headers?.['Authorization'];
|
||||
expect(authHeader).toBe('derp');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -523,15 +524,17 @@ describe('mcp-client', () => {
|
||||
},
|
||||
false,
|
||||
);
|
||||
expect(transport).toEqual(
|
||||
new SSEClientTransport(new URL('http://test-server'), {
|
||||
requestInit: { headers: {} },
|
||||
}),
|
||||
expect(transport).toBeInstanceOf(SSEClientTransport);
|
||||
expect(transport).toHaveProperty(
|
||||
'_url',
|
||||
new URL('http://test-server/'),
|
||||
);
|
||||
});
|
||||
|
||||
it('with headers', async () => {
|
||||
const transport = await createTransport(
|
||||
// We need this to be an any type because we dig into its private state.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const transport: any = await createTransport(
|
||||
'test-server',
|
||||
{
|
||||
url: 'http://test-server',
|
||||
@@ -539,14 +542,13 @@ describe('mcp-client', () => {
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
expect(transport).toEqual(
|
||||
new SSEClientTransport(new URL('http://test-server'), {
|
||||
requestInit: {
|
||||
headers: { Authorization: 'derp' },
|
||||
},
|
||||
}),
|
||||
expect(transport).toBeInstanceOf(SSEClientTransport);
|
||||
expect(transport).toHaveProperty(
|
||||
'_url',
|
||||
new URL('http://test-server/'),
|
||||
);
|
||||
const authHeader = transport._requestInit?.headers?.['Authorization'];
|
||||
expect(authHeader).toBe('derp');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { patchStdio, createInkStdio } from './stdio.js';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { coreEvents } from './events.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
vi.mock('./events.js', () => ({
|
||||
coreEvents: {
|
||||
emitOutput: vi.fn(),
|
||||
},
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { coreEvents } from './events.js';
|
||||
|
||||
// Capture the original stdout and stderr write methods before any monkey patching occurs.
|
||||
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { writeToStdout } from './stdio.js';
|
||||
|
||||
export function enableMouseEvents() {
|
||||
// Enable mouse tracking with SGR format
|
||||
// ?1002h = button event tracking (clicks + drags + scroll wheel)
|
||||
// ?1006h = SGR extended mouse mode (better coordinate handling)
|
||||
writeToStdout('\u001b[?1002h\u001b[?1006h');
|
||||
}
|
||||
|
||||
export function disableMouseEvents() {
|
||||
// Disable mouse tracking with SGR format
|
||||
writeToStdout('\u001b[?1006l\u001b[?1002l');
|
||||
}
|
||||
|
||||
export function enableKittyKeyboardProtocol() {
|
||||
writeToStdout('\x1b[>1u');
|
||||
}
|
||||
|
||||
export function disableKittyKeyboardProtocol() {
|
||||
writeToStdout('\x1b[<u');
|
||||
}
|
||||
|
||||
export function enableLineWrapping() {
|
||||
writeToStdout('\x1b[?7h');
|
||||
}
|
||||
|
||||
export function disableLineWrapping() {
|
||||
writeToStdout('\x1b[?7l');
|
||||
}
|
||||
|
||||
export function enterAlternateScreen() {
|
||||
writeToStdout('\x1b[?1049h');
|
||||
}
|
||||
|
||||
export function exitAlternateScreen() {
|
||||
writeToStdout('\x1b[?1049l');
|
||||
}
|
||||
|
||||
export function shouldEnterAlternateScreen(
|
||||
useAlternateBuffer: boolean,
|
||||
isScreenReader: boolean,
|
||||
): boolean {
|
||||
return useAlternateBuffer && !isScreenReader;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
This file contains third-party software notices and license terms.
|
||||
|
||||
============================================================
|
||||
@modelcontextprotocol/sdk@1.22.0
|
||||
@modelcontextprotocol/sdk@1.23.0
|
||||
(git+https://github.com/modelcontextprotocol/typescript-sdk.git)
|
||||
|
||||
MIT License
|
||||
@@ -2352,7 +2352,7 @@ SOFTWARE.
|
||||
|
||||
|
||||
============================================================
|
||||
zod-to-json-schema@3.24.6
|
||||
zod-to-json-schema@3.25.0
|
||||
(https://github.com/StefanTerdell/zod-to-json-schema)
|
||||
|
||||
ISC License
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.18.0-nightly.20251120.2231497b1",
|
||||
"version": "0.18.4",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
@@ -137,7 +137,7 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"zod": "^3.25.76"
|
||||
|
||||
@@ -292,8 +292,8 @@
|
||||
"useAlternateBuffer": {
|
||||
"title": "Use Alternate Screen Buffer",
|
||||
"description": "Use an alternate screen buffer for the UI, preserving shell history.",
|
||||
"markdownDescription": "Use an alternate screen buffer for the UI, preserving shell history.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`",
|
||||
"default": true,
|
||||
"markdownDescription": "Use an alternate screen buffer for the UI, preserving shell history.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `false`",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"incrementalRendering": {
|
||||
|
||||
Reference in New Issue
Block a user