Compare commits

...

7 Commits

28 changed files with 430 additions and 115 deletions
+22 -33
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"workspaces": [
"packages/*"
],
@@ -2301,7 +2301,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",
@@ -2482,7 +2481,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"
}
@@ -2516,7 +2514,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"
},
@@ -2885,7 +2882,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"
@@ -2919,7 +2915,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"
@@ -2972,7 +2967,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",
@@ -4154,7 +4148,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4442,7 +4435,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",
@@ -5210,7 +5202,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"
},
@@ -5565,7 +5556,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",
@@ -6811,6 +6803,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"
},
@@ -7817,7 +7810,6 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8407,6 +8399,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"
}
@@ -8416,6 +8409,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"
}
@@ -8425,6 +8419,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"
}
@@ -8654,6 +8649,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",
@@ -8672,6 +8668,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"
}
@@ -8680,13 +8677,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"
}
@@ -9891,7 +9890,6 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.3.tgz",
"integrity": "sha512-2qm05tjtdia+d1gD7LQjPJyCPJluKDuR5B+FI3ZZXshFoU1igZBFvXs2++x9OT6d9755q+gkRPOdtH8jzx5MiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -12943,7 +12941,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",
@@ -13477,7 +13476,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13488,7 +13486,6 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -15549,7 +15546,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15714,8 +15710,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",
@@ -15723,7 +15718,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"
@@ -15908,7 +15902,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16070,6 +16063,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"
}
@@ -16125,7 +16119,6 @@
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -16242,7 +16235,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16256,7 +16248,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -16954,7 +16945,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"
}
@@ -16970,7 +16960,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
@@ -17260,7 +17250,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.16.0",
@@ -17360,7 +17350,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
@@ -17495,7 +17485,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -17505,7 +17494,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -17516,7 +17505,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"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.16.0-nightly.20251113.ad1f0d99"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.16.0-preview.3"
},
"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.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.16.0-nightly.20251113.ad1f0d99"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.16.0-preview.3"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
+26 -3
View File
@@ -109,7 +109,7 @@ import { disableMouseEvents, enableMouseEvents } from './utils/mouse.js';
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
import { useSettings } from './contexts/SettingsContext.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
const WARNING_PROMPT_DURATION_MS = 1000;
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
@@ -892,6 +892,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
>();
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const [showIdeRestartPrompt, setShowIdeRestartPrompt] = useState(false);
const [selectionWarning, setSelectionWarning] = useState(false);
const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
@@ -901,6 +902,26 @@ Logging in with Google... Please restart Gemini CLI to continue.
} = useIdeTrustListener();
const isInitialMount = useRef(true);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
const handleSelectionWarning = () => {
setSelectionWarning(true);
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
setSelectionWarning(false);
}, WARNING_PROMPT_DURATION_MS);
};
appEvents.on(AppEvent.SelectionWarning, handleSelectionWarning);
return () => {
appEvents.off(AppEvent.SelectionWarning, handleSelectionWarning);
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);
useEffect(() => {
if (ideNeedsRestart) {
// IDE trust changed, force a restart.
@@ -976,7 +997,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
ctrlCTimerRef.current = setTimeout(() => {
setCtrlCPressCount(0);
ctrlCTimerRef.current = null;
}, CTRL_EXIT_PROMPT_DURATION_MS);
}, WARNING_PROMPT_DURATION_MS);
}
}, [ctrlCPressCount, config, setCtrlCPressCount, handleSlashCommand]);
@@ -994,7 +1015,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
ctrlDTimerRef.current = setTimeout(() => {
setCtrlDPressCount(0);
ctrlDTimerRef.current = null;
}, CTRL_EXIT_PROMPT_DURATION_MS);
}, WARNING_PROMPT_DURATION_MS);
}
}, [ctrlDPressCount, config, setCtrlDPressCount, handleSlashCommand]);
@@ -1345,6 +1366,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
embeddedShellFocused,
showDebugProfiler,
copyModeEnabled,
selectionWarning,
}),
[
isThemeDialogOpen,
@@ -1430,6 +1452,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
apiKeyDefaultValue,
authState,
copyModeEnabled,
selectionWarning,
],
);
@@ -99,6 +99,10 @@ export const Composer = () => {
<Text color={theme.status.warning}>
Press Ctrl+C again to exit.
</Text>
) : uiState.selectionWarning ? (
<Text color={theme.status.warning}>
Press Ctrl-S to enter selection mode to copy text.
</Text>
) : uiState.ctrlDPressedOnce ? (
<Text color={theme.status.warning}>
Press Ctrl+D again to exit.
@@ -60,4 +60,15 @@ describe('Help Component', () => {
expect(output).not.toContain('hidden-child');
unmount();
});
it('should render keyboard shortcuts', () => {
const { lastFrame, unmount } = render(<Help commands={mockCommands} />);
const output = lastFrame();
expect(output).toContain('Keyboard Shortcuts:');
expect(output).toContain('Ctrl+C');
expect(output).toContain('Ctrl+S');
expect(output).toContain('Page Up/Down');
unmount();
});
});
+12
View File
@@ -136,6 +136,12 @@ export const Help: React.FC<Help> = ({ commands }) => (
</Text>{' '}
- Clear the screen
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Ctrl+S
</Text>{' '}
- Enter selection mode to copy text
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
{process.platform === 'darwin' ? 'Ctrl+X / Meta+Enter' : 'Ctrl+X'}
@@ -160,6 +166,12 @@ export const Help: React.FC<Help> = ({ commands }) => (
</Text>{' '}
- Cancel operation / Clear input (double press)
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Page Up/Down
</Text>{' '}
- Scroll page up/down
</Text>
<Text color={theme.text.primary}>
<Text bold color={theme.text.accent}>
Shift+Tab
@@ -379,8 +379,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const relY = mouseY - y;
const visualRow = buffer.visualScrollRow + relY;
buffer.moveToVisualPosition(visualRow, relX);
return true;
}
}
return false;
},
[buffer],
);
@@ -11,6 +11,7 @@ import { vi, type Mock } from 'vitest';
import type React from 'react';
import { useStdin } from 'ink';
import { EventEmitter } from 'node:events';
import { appEvents, AppEvent } from '../../utils/events.js';
// Mock the 'ink' module to control stdin
vi.mock('ink', async (importOriginal) => {
@@ -21,6 +22,18 @@ vi.mock('ink', async (importOriginal) => {
};
});
// Mock appEvents
vi.mock('../../utils/events.js', () => ({
appEvents: {
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
},
AppEvent: {
SelectionWarning: 'selection-warning',
},
}));
class MockStdin extends EventEmitter {
isTTY = true;
setRawMode = vi.fn();
@@ -47,6 +60,7 @@ describe('MouseContext', () => {
wrapper = ({ children }: { children: React.ReactNode }) => (
<MouseProvider mouseEventsEnabled={true}>{children}</MouseProvider>
);
vi.mocked(appEvents.emit).mockClear();
});
afterEach(() => {
@@ -91,6 +105,34 @@ describe('MouseContext', () => {
expect(handler).not.toHaveBeenCalled();
});
it('should emit SelectionWarning when move event is unhandled and has coordinates', () => {
renderHook(() => useMouseContext(), { wrapper });
act(() => {
// Move event (32) at 10, 20
stdin.write('\x1b[<32;10;20M');
});
expect(appEvents.emit).toHaveBeenCalledWith(AppEvent.SelectionWarning);
});
it('should not emit SelectionWarning when move event is handled', () => {
const handler = vi.fn().mockReturnValue(true);
const { result } = renderHook(() => useMouseContext(), { wrapper });
act(() => {
result.current.subscribe(handler);
});
act(() => {
// Move event (32) at 10, 20
stdin.write('\x1b[<32;10;20M');
});
expect(handler).toHaveBeenCalled();
expect(appEvents.emit).not.toHaveBeenCalled();
});
describe('SGR Mouse Events', () => {
it.each([
{
+18 -1
View File
@@ -15,6 +15,7 @@ import {
} from 'react';
import { ESC } from '../utils/input.js';
import { debugLogger } from '@google/gemini-cli-core';
import { appEvents, AppEvent } from '../../utils/events.js';
import {
isIncompleteMouseSequence,
parseMouseEvent,
@@ -89,8 +90,24 @@ export function MouseProvider({
let mouseBuffer = '';
const broadcast = (event: MouseEvent) => {
let handled = false;
for (const handler of subscribers) {
handler(event);
if (handler(event) === true) {
handled = true;
}
}
if (
!handled &&
event.name === 'move' &&
event.col >= 0 &&
event.row >= 0 &&
event.button === 'left'
) {
// Terminal apps only receive mouse move events when the mouse is down
// so this always indicates a mouse drag that the user was expecting
// would trigger text selection but does not as we are handling mouse
// events not the terminal.
appEvents.emit(AppEvent.SelectionWarning);
}
};
@@ -113,6 +113,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -125,6 +126,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -142,6 +144,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -159,6 +162,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -172,6 +176,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
});
}
expect(scrollBy).not.toHaveBeenCalled();
@@ -209,6 +214,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -231,6 +237,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -268,6 +275,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -305,6 +313,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -342,6 +351,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -407,6 +417,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -424,6 +435,7 @@ describe('ScrollProvider Drag', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
// Expect scrollTo to be called with target and duration 0
@@ -16,12 +16,12 @@ import { Box, type DOMElement } from 'ink';
import type { MouseEvent } from '../hooks/useMouse.js';
// Mock useMouse hook
const mockUseMouseCallbacks = new Set<(event: MouseEvent) => void>();
const mockUseMouseCallbacks = new Set<(event: MouseEvent) => void | boolean>();
vi.mock('../hooks/useMouse.js', async () => {
// We need to import React dynamically because this factory runs before top-level imports
const React = await import('react');
return {
useMouse: (callback: (event: MouseEvent) => void) => {
useMouse: (callback: (event: MouseEvent) => void | boolean) => {
React.useEffect(() => {
mockUseMouseCallbacks.add(callback);
return () => {
@@ -81,6 +81,83 @@ describe('ScrollProvider', () => {
vi.useRealTimers();
});
describe('Event Handling Status', () => {
it('returns true when scroll event is handled', () => {
const scrollBy = vi.fn();
const getScrollState = vi.fn(() => ({
scrollTop: 0,
scrollHeight: 100,
innerHeight: 10,
}));
render(
<ScrollProvider>
<TestScrollable
id="test-scrollable"
scrollBy={scrollBy}
getScrollState={getScrollState}
/>
</ScrollProvider>,
);
let handled = false;
for (const callback of mockUseMouseCallbacks) {
if (
callback({
name: 'scroll-down',
col: 5,
row: 5,
shift: false,
ctrl: false,
meta: false,
button: 'none',
}) === true
) {
handled = true;
}
}
expect(handled).toBe(true);
});
it('returns false when scroll event is ignored (cannot scroll further)', () => {
const scrollBy = vi.fn();
// Already at bottom
const getScrollState = vi.fn(() => ({
scrollTop: 90,
scrollHeight: 100,
innerHeight: 10,
}));
render(
<ScrollProvider>
<TestScrollable
id="test-scrollable"
scrollBy={scrollBy}
getScrollState={getScrollState}
/>
</ScrollProvider>,
);
let handled = false;
for (const callback of mockUseMouseCallbacks) {
if (
callback({
name: 'scroll-down',
col: 5,
row: 5,
shift: false,
ctrl: false,
meta: false,
button: 'none',
}) === true
) {
handled = true;
}
}
expect(handled).toBe(false);
});
});
it('calls scrollTo when clicking scrollbar track if available', async () => {
const scrollBy = vi.fn();
const scrollTo = vi.fn();
@@ -118,6 +195,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -151,6 +229,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -183,6 +262,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
};
for (const callback of mockUseMouseCallbacks) {
callback(mouseEvent);
@@ -228,6 +308,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
});
callback({
name: 'scroll-down',
@@ -236,6 +317,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
});
callback({
name: 'scroll-up',
@@ -244,6 +326,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
});
}
@@ -283,6 +366,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
});
callback({
name: 'scroll-down',
@@ -291,6 +375,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
});
callback({
name: 'scroll-down',
@@ -299,6 +384,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'none',
});
}
@@ -342,6 +428,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -354,6 +441,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -366,6 +454,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -400,6 +489,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -412,6 +502,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
@@ -423,6 +514,7 @@ describe('ScrollProvider', () => {
shift: false,
ctrl: false,
meta: false,
button: 'left',
});
}
+23 -12
View File
@@ -146,15 +146,16 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({
if (direction === 'up' && canScrollUp) {
pendingScrollsRef.current.set(candidate.id, pendingDelta + delta);
scheduleFlush();
return;
return true;
}
if (direction === 'down' && canScrollDown) {
pendingScrollsRef.current.set(candidate.id, pendingDelta + delta);
scheduleFlush();
return;
return true;
}
}
return false;
};
const handleLeftPress = (mouseEvent: MouseEvent) => {
@@ -238,7 +239,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({
id: entry.id,
offset,
};
return;
return true;
}
}
@@ -250,21 +251,27 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({
if (candidates.length > 0) {
// The first candidate is the innermost one.
candidates[0].flashScrollbar();
// We don't consider just flashing the scrollbar as handling the event
// in a way that should prevent other handlers (like drag warning)
// from checking it, although for left-press it doesn't matter much.
// But returning false is safer.
return false;
}
return false;
};
const handleMove = (mouseEvent: MouseEvent) => {
const state = dragStateRef.current;
if (!state.active || !state.id) return;
if (!state.active || !state.id) return false;
const entry = scrollablesRef.current.get(state.id);
if (!entry || !entry.ref.current) {
state.active = false;
return;
return false;
}
const boundingBox = getBoundingBox(entry.ref.current);
if (!boundingBox) return;
if (!boundingBox) return false;
const { y } = boundingBox;
const { scrollTop, scrollHeight, innerHeight } = entry.getScrollState();
@@ -276,7 +283,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({
const maxScrollTop = scrollHeight - innerHeight;
const maxThumbY = innerHeight - thumbHeight;
if (maxThumbY <= 0) return;
if (maxThumbY <= 0) return false;
const relativeMouseY = mouseEvent.row - y;
// Calculate the target thumb position based on the mouse position and the offset.
@@ -295,6 +302,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({
} else {
entry.scrollBy(targetScrollTop - scrollTop);
}
return true;
};
const handleLeftRelease = () => {
@@ -304,22 +312,25 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({
id: null,
offset: 0,
};
return true;
}
return false;
};
useMouse(
(event: MouseEvent) => {
if (event.name === 'scroll-up') {
handleScroll('up', event);
return handleScroll('up', event);
} else if (event.name === 'scroll-down') {
handleScroll('down', event);
return handleScroll('down', event);
} else if (event.name === 'left-press') {
handleLeftPress(event);
return handleLeftPress(event);
} else if (event.name === 'move') {
handleMove(event);
return handleMove(event);
} else if (event.name === 'left-release') {
handleLeftRelease();
return handleLeftRelease();
}
return false;
},
{ isActive: true },
);
@@ -124,6 +124,7 @@ export interface UIState {
showDebugProfiler: boolean;
showFullTodos: boolean;
copyModeEnabled: boolean;
selectionWarning: boolean;
}
export const UIStateContext = createContext<UIState | null>(null);
@@ -28,8 +28,27 @@ import {
} from '@google/gemini-cli-core';
import { appEvents } from '../../utils/events.js';
const { logSlashCommand } = vi.hoisted(() => ({
const {
logSlashCommand,
mockBuiltinLoadCommands,
mockFileLoadCommands,
mockMcpLoadCommands,
mockIdeClientGetInstance,
mockUseAlternateBuffer,
} = vi.hoisted(() => ({
logSlashCommand: vi.fn(),
mockBuiltinLoadCommands: vi.fn().mockResolvedValue([]),
mockFileLoadCommands: vi.fn().mockResolvedValue([]),
mockMcpLoadCommands: vi.fn().mockResolvedValue([]),
mockIdeClientGetInstance: vi.fn().mockResolvedValue({
addStatusChangeListener: vi.fn(),
removeStatusChangeListener: vi.fn(),
}),
mockUseAlternateBuffer: vi.fn().mockReturnValue(false),
}));
vi.mock('./useAlternateBuffer.js', () => ({
useAlternateBuffer: mockUseAlternateBuffer,
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -41,10 +60,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
logSlashCommand,
getIdeInstaller: vi.fn().mockReturnValue(null),
IdeClient: {
getInstance: vi.fn().mockResolvedValue({
addStatusChangeListener: vi.fn(),
removeStatusChangeListener: vi.fn(),
}),
getInstance: mockIdeClientGetInstance,
},
};
});
@@ -65,23 +81,20 @@ vi.mock('node:process', () => {
};
});
const mockBuiltinLoadCommands = vi.fn();
vi.mock('../../services/BuiltinCommandLoader.js', () => ({
BuiltinCommandLoader: vi.fn().mockImplementation(() => ({
BuiltinCommandLoader: vi.fn(() => ({
loadCommands: mockBuiltinLoadCommands,
})),
}));
const mockFileLoadCommands = vi.fn();
vi.mock('../../services/FileCommandLoader.js', () => ({
FileCommandLoader: vi.fn().mockImplementation(() => ({
FileCommandLoader: vi.fn(() => ({
loadCommands: mockFileLoadCommands,
})),
}));
const mockMcpLoadCommands = vi.fn();
vi.mock('../../services/McpPromptLoader.js', () => ({
McpPromptLoader: vi.fn().mockImplementation(() => ({
McpPromptLoader: vi.fn(() => ({
loadCommands: mockMcpLoadCommands,
})),
}));
@@ -130,6 +143,12 @@ describe('useSlashCommandProcessor', () => {
mockBuiltinLoadCommands.mockResolvedValue([]);
mockFileLoadCommands.mockResolvedValue([]);
mockMcpLoadCommands.mockResolvedValue([]);
mockUseAlternateBuffer.mockReturnValue(false);
mockIdeClientGetInstance.mockResolvedValue({
addStatusChangeListener: vi.fn(),
removeStatusChangeListener: vi.fn(),
});
vi.spyOn(console, 'clear').mockImplementation(() => {});
});
afterEach(async () => {
@@ -137,6 +156,7 @@ describe('useSlashCommandProcessor', () => {
await unmountHook();
unmountHook = undefined;
}
vi.restoreAllMocks();
});
const setupProcessorHook = async (
@@ -205,6 +225,44 @@ describe('useSlashCommandProcessor', () => {
};
};
describe('Console Clear Safety', () => {
it('should not call console.clear if alternate buffer is active', async () => {
mockUseAlternateBuffer.mockReturnValue(true);
const clearCommand = createTestCommand({
name: 'clear',
action: async (context) => {
context.ui.clear();
},
});
const result = await setupProcessorHook([clearCommand]);
await act(async () => {
await result.current.handleSlashCommand('/clear');
});
expect(mockClearItems).toHaveBeenCalled();
expect(console.clear).not.toHaveBeenCalled();
});
it('should call console.clear if alternate buffer is not active', async () => {
mockUseAlternateBuffer.mockReturnValue(false);
const clearCommand = createTestCommand({
name: 'clear',
action: async (context) => {
context.ui.clear();
},
});
const result = await setupProcessorHook([clearCommand]);
await act(async () => {
await result.current.handleSlashCommand('/clear');
});
expect(mockClearItems).toHaveBeenCalled();
expect(console.clear).toHaveBeenCalled();
});
});
describe('Initialization and Command Loading', () => {
it('should initialize CommandService with all required loaders', async () => {
await setupProcessorHook();
@@ -947,36 +1005,37 @@ describe('useSlashCommandProcessor', () => {
describe('Slash Command Logging', () => {
const mockCommandAction = vi.fn().mockResolvedValue({ type: 'handled' });
const loggingTestCommands: SlashCommand[] = [
createTestCommand({
name: 'logtest',
action: vi
.fn()
.mockResolvedValue({ type: 'message', content: 'hello world' }),
}),
createTestCommand({
name: 'logwithsub',
subCommands: [
createTestCommand({
name: 'sub',
action: mockCommandAction,
}),
],
}),
createTestCommand({
name: 'fail',
action: vi.fn().mockRejectedValue(new Error('oh no!')),
}),
createTestCommand({
name: 'logalias',
altNames: ['la'],
action: mockCommandAction,
}),
];
let loggingTestCommands: SlashCommand[];
beforeEach(() => {
mockCommandAction.mockClear();
vi.mocked(logSlashCommand).mockClear();
loggingTestCommands = [
createTestCommand({
name: 'logtest',
action: vi
.fn()
.mockResolvedValue({ type: 'message', content: 'hello world' }),
}),
createTestCommand({
name: 'logwithsub',
subCommands: [
createTestCommand({
name: 'sub',
action: mockCommandAction,
}),
],
}),
createTestCommand({
name: 'fail',
action: vi.fn().mockRejectedValue(new Error('oh no!')),
}),
createTestCommand({
name: 'logalias',
altNames: ['la'],
action: mockCommandAction,
}),
];
});
it.each([
@@ -44,6 +44,7 @@ import {
type ExtensionUpdateStatus,
} from '../state/extensions.js';
import { appEvents } from '../../utils/events.js';
import { useAlternateBuffer } from './useAlternateBuffer.js';
interface SlashCommandProcessorActions {
openAuthDialog: () => void;
@@ -81,6 +82,7 @@ export const useSlashCommandProcessor = (
const [commands, setCommands] = useState<readonly SlashCommand[] | undefined>(
undefined,
);
const alternateBuffer = useAlternateBuffer();
const [reloadTrigger, setReloadTrigger] = useState(0);
const reloadCommands = useCallback(() => {
@@ -196,7 +198,9 @@ export const useSlashCommandProcessor = (
addItem,
clear: () => {
clearItems();
console.clear();
if (!alternateBuffer) {
console.clear();
}
refreshStatic();
},
loadHistory,
@@ -218,6 +222,7 @@ export const useSlashCommandProcessor = (
},
}),
[
alternateBuffer,
config,
settings,
gitService,
+5
View File
@@ -27,6 +27,7 @@ describe('mouse utils', () => {
shift: false,
meta: false,
ctrl: false,
button: 'left',
});
expect(result!.length).toBe(input.length);
});
@@ -43,6 +44,7 @@ describe('mouse utils', () => {
shift: false,
meta: false,
ctrl: false,
button: 'left',
});
});
@@ -58,6 +60,7 @@ describe('mouse utils', () => {
shift: true,
meta: true,
ctrl: true,
button: 'left',
});
});
@@ -67,6 +70,7 @@ describe('mouse utils', () => {
const result = parseSGRMouseEvent(input);
expect(result).not.toBeNull();
expect(result!.event.name).toBe('move');
expect(result!.event.button).toBe('left');
});
it('parses SGR scroll events', () => {
@@ -100,6 +104,7 @@ describe('mouse utils', () => {
shift: false,
meta: false,
ctrl: false,
button: 'left',
});
expect(result!.length).toBe(6);
});
+31 -2
View File
@@ -33,9 +33,10 @@ export interface MouseEvent {
shift: boolean;
meta: boolean;
ctrl: boolean;
button: 'left' | 'middle' | 'right' | 'none';
}
export type MouseHandler = (event: MouseEvent) => void;
export type MouseHandler = (event: MouseEvent) => void | boolean;
export function getMouseEventName(
buttonCode: number,
@@ -71,6 +72,20 @@ export function getMouseEventName(
}
}
function getButtonFromCode(code: number): MouseEvent['button'] {
const button = code & 3;
switch (button) {
case 0:
return 'left';
case 1:
return 'middle';
case 2:
return 'right';
default:
return 'none';
}
}
export function parseSGRMouseEvent(
buffer: string,
): { event: MouseEvent; length: number } | null {
@@ -98,6 +113,7 @@ export function parseSGRMouseEvent(
shift,
col,
row,
button: getButtonFromCode(buttonCode),
},
length: match[0].length,
};
@@ -165,8 +181,21 @@ export function parseX11MouseEvent(
}
if (name) {
let button = getButtonFromCode(b);
if (name === 'left-release' && button === 'none') {
button = 'left';
}
return {
event: { name, ctrl, meta, shift, col, row },
event: {
name,
ctrl,
meta,
shift,
col,
row,
button,
},
length: match[0].length,
};
}
+2
View File
@@ -13,6 +13,7 @@ export enum AppEvent {
OauthDisplayMessage = 'oauth-display-message',
Flicker = 'flicker',
McpClientUpdate = 'mcp-client-update',
SelectionWarning = 'selection-warning',
}
export interface AppEvents extends ExtensionEvents {
@@ -21,6 +22,7 @@ export interface AppEvents extends ExtensionEvents {
[AppEvent.OauthDisplayMessage]: string[];
[AppEvent.Flicker]: never[];
[AppEvent.McpClientUpdate]: Array<Map<string, McpClient> | never>;
[AppEvent.SelectionWarning]: never[];
}
export const appEvents = new EventEmitter<AppEvents>();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"description": "Gemini CLI Core",
"repository": {
"type": "git",
@@ -42,8 +42,8 @@ describe('experiments', () => {
const { getExperiments } = await import('./experiments.js');
const mockApiResponse: ListExperimentsResponse = {
flags: [
{ name: 'flag1', boolValue: true },
{ name: 'flag2', stringValue: 'value' },
{ flagId: 234, boolValue: true },
{ flagId: 345, stringValue: 'value' },
],
experimentIds: [123, 456],
};
@@ -58,12 +58,12 @@ describe('experiments', () => {
);
// Verify that the response was parsed correctly
expect(experiments.flags['flag1']).toEqual({
name: 'flag1',
expect(experiments.flags[234]).toEqual({
flagId: 234,
boolValue: true,
});
expect(experiments.flags['flag2']).toEqual({
name: 'flag2',
expect(experiments.flags[345]).toEqual({
flagId: 345,
stringValue: 'value',
});
expect(experiments.experimentIds).toEqual([123, 456]);
@@ -85,7 +85,7 @@ describe('experiments', () => {
const mockApiResponse: ListExperimentsResponse = {
flags: [
{ boolValue: true } as Flag, // No name
{ name: 'flag2', stringValue: 'value' },
{ flagId: 256, stringValue: 'value' },
],
};
vi.mocked(mockServer.listExperiments).mockResolvedValue(mockApiResponse);
@@ -93,7 +93,7 @@ describe('experiments', () => {
const experiments = await getExperiments(mockServer);
expect(Object.keys(experiments.flags)).toHaveLength(1);
expect(experiments.flags['flag2']).toBeDefined();
expect(experiments.flags[256]).toBeDefined();
expect(experiments.flags['undefined']).toBeUndefined();
});
@@ -38,8 +38,8 @@ export async function getExperiments(
function parseExperiments(response: ListExperimentsResponse): Experiments {
const flags: Record<string, Flag> = {};
for (const flag of response.flags ?? []) {
if (flag.name) {
flags[flag.name] = flag;
if (flag.flagId) {
flags[flag.flagId] = flag;
}
}
return {
@@ -5,9 +5,8 @@
*/
export const ExperimentFlags = {
CONTEXT_COMPRESSION_THRESHOLD:
'GeminiCLIContextCompression__threshold_fraction',
USER_CACHING: 'GcliUserCaching__user_caching',
CONTEXT_COMPRESSION_THRESHOLD: 45740197,
USER_CACHING: 45740198,
} as const;
export type ExperimentFlagName =
@@ -19,7 +19,7 @@ export interface ListExperimentsResponse {
}
export interface Flag {
name?: string;
flagId?: number;
boolValue?: boolean;
floatValue?: number;
intValue?: string; // int64
+2 -2
View File
@@ -1425,8 +1425,8 @@ export class Config {
this.experiments = experiments;
const flagSummaries = Object.entries(experiments.flags ?? {})
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, flag]) => {
const summary: Record<string, unknown> = { name };
.map(([flagId, flag]) => {
const summary: Record<string, unknown> = { flagId };
if (flag.boolValue !== undefined) {
summary['boolValue'] = flag.boolValue;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"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.16.0-nightly.20251113.ad1f0d99",
"version": "0.16.0-preview.3",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {