Compare commits

...

10 Commits

Author SHA1 Message Date
gemini-cli-robot f01890b073 chore(release): v0.18.0 2025-11-26 02:50:41 +00:00
gemini-cli-robot 7beccfa03c chore(release): v0.18.0-preview.4 2025-11-25 22:02:36 +00:00
gemini-cli-robot a640766f10 fix(patch): cherry-pick d8a3d08 to release/v0.18.0-preview.3-pr-13791 to patch version v0.18.0-preview.3 and create version 0.18.0-preview.4 (#13826)
Co-authored-by: Sehoon Shon <sshon@google.com>
2025-11-25 21:42:55 +00:00
gemini-cli-robot 9f55fb5010 chore(release): v0.18.0-preview.3 2025-11-25 19:31:29 +00:00
gemini-cli-robot ea3d022c8b fix(patch): cherry-pick 3e50be1 to release/v0.18.0-preview.2-pr-13428 to patch version v0.18.0-preview.2 and create version 0.18.0-preview.3 (#13821)
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
2025-11-25 11:10:54 -08:00
gemini-cli-robot 313688fd18 chore(release): v0.18.0-preview.2 2025-11-25 18:14:40 +00:00
gemini-cli-robot 843b019cef fix(patch): cherry-pick d351f07 to release/v0.18.0-preview.1-pr-12535 to patch version v0.18.0-preview.1 and create version 0.18.0-preview.2 (#13813)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2025-11-25 17:20:20 +00:00
gemini-cli-robot 80ef6f854b chore(release): v0.18.0-preview.1 2025-11-22 02:14:04 +00:00
gemini-cli-robot 316349ca61 fix(patch): cherry-pick 5e218a5 to release/v0.18.0-preview.0-pr-13623 to patch version v0.18.0-preview.0 and create version 0.18.0-preview.1 (#13626)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2025-11-22 01:58:57 +00:00
gemini-cli-robot 179010eb3b chore(release): v0.18.0-preview.0 2025-11-20 22:19:18 +00:00
37 changed files with 923 additions and 223 deletions
+1 -1
View File
@@ -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):
+3
View File
@@ -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,
+22 -33
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"workspaces": [
"packages/*"
],
@@ -2403,7 +2403,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 +2583,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 +2616,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 +2984,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 +3017,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 +3069,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 +4279,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4573,7 +4566,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 +5489,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 +5924,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 +7189,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 +8205,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 +8794,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 +8804,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 +8814,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 +9068,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 +9087,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 +9096,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 +10315,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 +13422,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 +13958,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 +13968,6 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -16189,7 +16186,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16354,8 +16350,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 +16358,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 +16542,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16711,6 +16704,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 +16760,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -16883,7 +16876,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16897,7 +16889,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -17604,7 +17595,6 @@
"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"
}
@@ -17620,7 +17610,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
@@ -17910,7 +17900,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
@@ -18011,7 +18001,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
@@ -18146,7 +18136,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 +18145,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -18167,7 +18156,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"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.0"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"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.0"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
+1 -1
View File
@@ -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,
+1
View File
@@ -512,6 +512,7 @@ describe('startInteractiveUI', () => {
merged: {
ui: {
hideWindowTitle: false,
useAlternateBuffer: true,
},
},
} as LoadedSettings;
+1 -1
View File
@@ -176,7 +176,7 @@ export const renderWithProviders = (
width,
mouseEventsEnabled = false,
config = configProxy as unknown as Config,
useAlternateBuffer,
useAlternateBuffer = true,
uiActions,
}: {
shellFocus?: boolean;
+3
View File
@@ -705,6 +705,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
handleApprovalModeChange,
activePtyId,
loopDetectionConfirmationRequest,
lastOutputTime,
} = useGeminiStream(
config.getGeminiClient(),
historyManager.history,
@@ -1104,6 +1105,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(
@@ -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();
});
+3 -24
View File
@@ -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 !== ''}
/>
)}
</>
@@ -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>
);
);
};
+2
View File
@@ -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();
+147
View File
@@ -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');
});
});
+73
View File
@@ -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,
};
}
+19 -13
View File
@@ -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} />,
+67 -46
View File
@@ -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,
];
}
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"description": "Gemini CLI Core",
"repository": {
"type": "git",
+165 -26
View File
@@ -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 () => {
+12 -8
View File
@@ -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);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.18.0-nightly.20251120.2231497b1",
"version": "0.18.0",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -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.0",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {
+2 -2
View File
@@ -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": {